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..121d5b38ffaa --- /dev/null +++ b/.github/actions/trivy-triage/helpers.js @@ -0,0 +1,69 @@ +module.exports = { + detectDiscussionLabels: (discussion, configDiscussionLabels) => { + res = []; + const discussionId = discussion.id; + const category = discussion.category.name; + const body = discussion.body; + if (category !== "Ideas") { + consolt.log("skipping discussion with category ${category} and body ${body}"); + } + const scannerPattern = /### Scanner\n\n(.+)/; + const scannerFound = body.match(scannerPattern); + if (scannerFound && scannerFound.length > 1) { + res.push(configDiscussionLabels[scannerFound[1]]); + } + const targetPattern = /### Target\n\n(.+)/; + const targetFound = body.match(targetPattern); + if (targetFound && targetFound.length > 1) { + res.push(configDiscussionLabels[targetFound[1]]); + } + 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..3ef2ef810124 --- /dev/null +++ b/.github/actions/trivy-triage/helpers.test.js @@ -0,0 +1,77 @@ +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('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')); + }); + }); +}); 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/trivy-triage.yaml b/.github/trivy-triage.yaml new file mode 100644 index 000000000000..e0ddc568f23e --- /dev/null +++ b/.github/trivy-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@v4 + - uses: ./.github/actions/trivy-triage + with: + discussion_num: ${{ github.event.inputs.discussion_num }}