Skip to content

Commit

Permalink
Issue for distinct revisions when vulnerabilities are found (#295)
Browse files Browse the repository at this point in the history
* feat: report vul scan findings by revisions and affected tags

* fix: install pydantic dep

* feat: add from-release and summary

* ci: automatically update oci/mock-rock/_releases.json, from https://github.com/canonical/oci-factory/actions/runs/11958857638

* fix: boolean conditioning

* ci: automatically update oci/mock-rock/_releases.json, from https://github.com/canonical/oci-factory/actions/runs/11973170999

* chore: code cleanup

* chore: use issue-title for consistency

* ci: automatically update oci/mock-rock/_releases.json, from https://github.com/canonical/oci-factory/actions/runs/11974360191

* fix: include revision in the issue title

* TEST: mock the cont. testing workflow with dispatch

* fix: ture to be bool

* fix: boolean conditioning

* fix: issue body formatting

* Revert "TEST: mock the cont. testing workflow with dispatch"

This reverts commit 721ce40.

* chore: apply suggestions from Clay

* ci: automatically update oci/mock-rock/_releases.json, from https://github.com/canonical/oci-factory/actions/runs/12012655556

---------

Co-authored-by: zhijie-yang <zhijie-yang@localhost>
  • Loading branch information
zhijie-yang and zhijie-yang authored Nov 26, 2024
1 parent 21bfda1 commit 44b7e9c
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 23 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/Continuous-Testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ jobs:
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.prepare-test-matrix.outputs.released-revisions-matrix) }}
uses: canonical/oci-factory/.github/workflows/Vulnerability-Scan.yaml@main
uses: ./.github/workflows/Vulnerability-Scan.yaml
with:
oci-image-name: "${{ matrix.source-image }}"
oci-image-path: "oci/${{ matrix.name }}"
date-last-scan: ${{ needs.prepare-test-matrix.outputs.last-scan }}
is-from-release: true
secrets: inherit
63 changes: 48 additions & 15 deletions .github/workflows/Vulnerability-Scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ on:
required: false
type: string
default: '9999-12-31T23:59:59'
create-issue:
description: 'If to create a GitHub issues for found vulnerabilities'
required: false
type: boolean
default: false

env:
TEST_IMAGE_NAME: 'test-img'
Expand Down Expand Up @@ -203,11 +208,19 @@ jobs:
if: ${{ !cancelled() && github.event_name != 'pull_request' }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.x'

- run: pip install pydantic==2.8.2

- id: simplify-image-name
run: |
img_name=$(echo "${{ inputs.oci-image-name }}" | sed -r 's|.*/([a-zA-Z0-9-]+:[0-9.-]+)_[0-9]+|\1|')
echo "img_name=$img_name" >> "$GITHUB_OUTPUT"
img_name_with_tag=$(echo "${{ inputs.oci-image-name }}" | sed -r 's|.*/([a-zA-Z0-9-]+:[0-9.-]+_[0-9])+|\1|')
img_revision=$(echo "${img_name_with_tag}" | cut -d '_' -f 2)
echo "img_revision=$img_revision" >> "$GITHUB_OUTPUT"
echo "img_name_with_tag=$img_name_with_tag" >> "$GITHUB_OUTPUT"
# We assume that the sources within image.yaml are the same
- name: Get image repo
Expand All @@ -217,22 +230,38 @@ jobs:
echo "img-repo=$img_repo" >> "$GITHUB_OUTPUT"
# We have to walk through the vulnerabilities since trivy does not support outputting the results as Markdown
- name: Create Markdown Content
- name: Create markdown content
id: create-markdown
run: |
set -x
title="Vulnerabilities found for ${{ steps.simplify-image-name.outputs.img_name }}"
echo "## $title" > issue.md
echo "| ID | Target | Severity | Package |" >> issue.md
echo "| -- | ----- | -------- | ------- |" >> issue.md
echo '${{ needs.test-vulnerabilities.outputs.vulnerabilities }}' | jq -r '.[] | "| \(.VulnerabilityID) | /\(.Target) | \(.Severity) | \(.PkgName) |"' >> issue.md
echo -e "\nDetails: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> issue.md
num_vulns=$(echo '${{ needs.test-vulnerabilities.outputs.vulnerabilities }}' | jq -r 'length')
echo "issue-title=$title" >> "$GITHUB_OUTPUT"
echo "issue-body-file=issue.md" >> "$GITHUB_OUTPUT"
echo "vulnerability-exists=$([[ $num_vulns -gt 0 ]] && echo 'true' || echo 'false')" >> "$GITHUB_OUTPUT"
vulnerability_exists=$([[ $num_vulns -gt 0 ]] && echo 'true' || echo 'false')
echo "vulnerability-exists=$vulnerability_exists" >> "$GITHUB_OUTPUT"
if [[ $vulnerability_exists == 'true' ]]; then
title="Vulnerabilities found for ${{ steps.simplify-image-name.outputs.img_name_with_tag }}"
echo "## $title" > issue.md
echo "| ID | Target | Severity | Package |" >> issue.md
echo "| -- | ----- | -------- | ------- |" >> issue.md
echo '${{ needs.test-vulnerabilities.outputs.vulnerabilities }}' | jq -r '.[] | "| \(.VulnerabilityID) | /\(.Target) | \(.Severity) | \(.PkgName) |"' >> issue.md
if [[ ${{ inputs.create-issue }} == 'true' ]]; then
revision_to_released_tags=$(python3 -m src.shared.release_info get_revision_to_released_tags --all-releases ${{ inputs.oci-image-path }}/_releases.json)
affected_tracks=$(echo "${revision_to_released_tags}" | jq -r '."${{ steps.simplify-image-name.outputs.img_revision }}" | map("- `\(.)`") | join("\n")')
echo -e "\n### Affected tracks:" >> issue.md
echo -e "${affected_tracks}" >> issue.md
echo -e "\nDetails: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> issue.md
fi
echo "issue-title=$title" >> "$GITHUB_OUTPUT"
echo "issue-body-file=issue.md" >> "$GITHUB_OUTPUT"
fi
- name: Write to summary
if: ${{ !inputs.create-issue && steps.create-markdown.outputs.vulnerability-exists == 'true' }}
run: |
echo "# Vulnerabilities found for ${{ inputs.oci-image-name }}" >> $GITHUB_STEP_SUMMARY
cat ${{ steps.create-markdown.outputs.issue-body-file }} | tail -n +2 >> $GITHUB_STEP_SUMMARY
- id: issue-exists
if: ${{ inputs.create-issue}}
run: |
issue_number=$(gh issue list --repo ${{ steps.get-image-repo.outputs.img-repo }} --json "number,title" \
| jq -r '.[] | select(.title == "${{ steps.create-markdown.outputs.issue-title }}") | .number')
Expand All @@ -253,7 +282,7 @@ jobs:
# | F | F | F | nop |

- name: Notify via GitHub issue
if: ${{ steps.create-markdown.outputs.vulnerability-exists == 'true' }}
if: ${{ steps.create-markdown.outputs.vulnerability-exists == 'true' && inputs.create-issue }}
run: |
set -x
op=nop
Expand All @@ -265,11 +294,15 @@ jobs:
fi
if [[ $op != 'nop' ]]; then
gh issue $op --repo ${{ steps.get-image-repo.outputs.img-repo }} \
--title "Vulnerabilities found for ${{ steps.simplify-image-name.outputs.img_name }}" \
--title "${{ steps.create-markdown.outputs.issue-title }}" \
--body-file "${{ steps.create-markdown.outputs.issue-body-file }}"
fi
- name: Close issue
if: ${{ needs.test-vulnerabilities.result == 'success' && steps.issue-exists.outputs.issue-exists == 'true' && steps.create-markdown.outputs.vulnerability-exists == 'false' }}
if: |
needs.test-vulnerabilities.result == 'success' &&
steps.issue-exists.outputs.issue-exists == 'true' &&
steps.create-markdown.outputs.vulnerability-exists == 'false' &&
inputs.create-issue
run: |
gh issue close ${{ steps.issue-exists.outputs.issue-number }} --repo ${{ steps.get-image-repo.outputs.img-repo }}
14 changes: 7 additions & 7 deletions oci/mock-rock/_releases.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,31 @@
"1.1-22.04": {
"end-of-life": "2030-05-01T00:00:00Z",
"candidate": {
"target": "806"
"target": "882"
},
"beta": {
"target": "806"
"target": "882"
},
"edge": {
"target": "806"
"target": "882"
}
},
"1-22.04": {
"end-of-life": "2030-05-01T00:00:00Z",
"candidate": {
"target": "806"
"target": "882"
},
"beta": {
"target": "806"
"target": "882"
},
"edge": {
"target": "806"
"target": "882"
}
},
"1.2-22.04": {
"end-of-life": "2030-05-01T00:00:00Z",
"beta": {
"target": "807"
"target": "883"
},
"edge": {
"target": "1.2-22.04_beta"
Expand Down
65 changes: 65 additions & 0 deletions src/shared/release_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
data related to _release.json and revision tags.
"""

import argparse
import json
from collections import defaultdict

from ..image.utils.schema.triggers import KNOWN_RISKS_ORDERED


Expand Down Expand Up @@ -82,3 +85,65 @@ def get_revision_to_track(all_revisions_tags: list) -> dict:

revision_track[revision] = track
return revision_track


def _find_alias_revision(tag_mapping_from_all_releases: dict, rev: str, visited: set, tag: str) -> str:
if rev in visited:
raise BadChannel(
f"Tag {tag} was caught in a circular dependency, "
"following tags that follow themselves. Cannot pin a revision."
)
visited.add(rev)
if not rev.isdigit():
return _find_alias_revision(
tag_mapping_from_all_releases, tag_mapping_from_all_releases[rev], visited, tag
)
return rev

def get_revision_to_released_tags(all_releases: dict) -> dict:
"""
Iterates over the provided dictionary with all the releases
and extracts the revision numbers and their corresponding
released tags. The resulting dictionary maps each revision
number to a list of released tags.
"""
revision_to_released_tags = defaultdict(list)
tag_mapping_from_all_releases = get_tag_mapping_from_all_releases(all_releases)
for tag, revision in tag_mapping_from_all_releases.items():
if not revision.isdigit():
visited = set()
revision = _find_alias_revision(tag_mapping_from_all_releases, revision, visited, tag)
revision = int(revision)
revision_to_released_tags[revision].append(tag)

for revision, tags in revision_to_released_tags.items():
revision_to_released_tags[revision] = sorted(tags)

return dict(revision_to_released_tags)


def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"function",
help="The function to run",
choices=["get_revision_to_released_tags"],
)

parser.add_argument(
"--all-releases",
help="Path to the _releases.json file",
)

args = parser.parse_args()

if args.function == "get_revision_to_released_tags":
print(
json.dumps(
(get_revision_to_released_tags(read_json_file(args.all_releases)))
)
)


if __name__ == "__main__":
main()
63 changes: 63 additions & 0 deletions tests/unit/test_shared_release_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pytest

from src.shared.release_info import *


def test_get_revision_to_release_plain():
all_releases = {
"3.8-20.04": {
"end-of-life": "2025-03-31T00:00:00Z",
"edge": {"target": "43"},
"stable": {"target": "43"},
"candidate": {"target": "43"},
"beta": {"target": "43"},
}
}
assert get_revision_to_released_tags(all_releases) == {
43: [
"3.8-20.04_beta",
"3.8-20.04_candidate",
"3.8-20.04_edge",
"3.8-20.04_stable",
]
}


def test_get_revision_to_release_circular():
all_releases = {
"1.19.0-22.04": {
"end-of-life": "2024-11-26T00:00:00Z",
"stable": {"target": "1"},
"candidate": {"target": "1.19.0-22.04_beta"},
"beta": {"target": "1.19.0-22.04_candidate"},
"edge": {"target": "5"},
}
}

with pytest.raises(BadChannel, match=r"Tag .* was caught in a circular dependency, following tags that follow themselves. Cannot pin a revision."):
get_revision_to_released_tags(all_releases)


def test_get_revision_to_release_alias():
all_releases = {
"1.19.0-22.04": {
"end-of-life": "2024-11-26T00:00:00Z",
"stable": {"target": "1"},
"candidate": {"target": "5"},
"beta": {"target": "1.19.0-22.04_candidate"},
"edge": {"target": "5"},
},
"1-22.04": {
"end-of-life": "2025-05-12T00:00:00Z",
"stable": {"target": "4"},
"candidate": {"target": "1-22.04_stable"},
"beta": {"target": "1-22.04_candidate"},
"edge": {"target": "1-22.04_beta"},
},
}

assert get_revision_to_released_tags(all_releases) == {
1: ["1.19.0-22.04_stable"],
4: ["1-22.04_beta", "1-22.04_candidate", "1-22.04_edge", "1-22.04_stable"],
5: ["1.19.0-22.04_beta", "1.19.0-22.04_candidate", "1.19.0-22.04_edge"],
}

0 comments on commit 44b7e9c

Please sign in to comment.