From 078e77e8177a36d55da0685d9dc571f7c177b09f Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 11:54:43 +0400 Subject: [PATCH 01/12] Split code scan into backend and frontend jobs gated by changes --- .github/workflows/test.yml | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40e610f3d0..9b9489f403 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,14 +29,14 @@ jobs: if: github.event.pull_request.state == 'open' && !github.event.pull_request.draft - scan: - name: "Scan Code" + scan-backend: + name: "Scan Code: backend" runs-on: ubuntu-latest needs: [changes] permissions: contents: read packages: read - if: github.event_name == 'pull_request' + if: fromJSON(needs.changes.outputs.changes || '{}').java == 'true' || fromJSON(needs.changes.outputs.changes || '{}').rust == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -48,6 +48,30 @@ jobs: uses: ./.github/steps/trivy with: scan: code + skip-dirs: openframe-frontend-core + image-name: backend + + scan-frontend: + name: "Scan Code: frontend" + runs-on: ubuntu-latest + needs: [changes] + permissions: + contents: read + packages: read + if: fromJSON(needs.changes.outputs.changes || '{}')['openframe-frontend-core'] == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Scan code + uses: ./.github/steps/trivy + with: + scan: code + path: openframe-frontend-core + image-name: frontend test-java: name: Test Java From d690cfcbae9333daff73319b8cff176716532fa9 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 11:56:38 +0400 Subject: [PATCH 02/12] Trigger both scans --- openframe-frontend-core/package.json | 1 + pom.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/openframe-frontend-core/package.json b/openframe-frontend-core/package.json index da6525bfc2..b426eb0445 100644 --- a/openframe-frontend-core/package.json +++ b/openframe-frontend-core/package.json @@ -532,3 +532,4 @@ ] } } + diff --git a/pom.xml b/pom.xml index c0d6c142e6..96b12d8b9a 100644 --- a/pom.xml +++ b/pom.xml @@ -343,3 +343,4 @@ + From eeadc25ea76e51eb23f0bacbbf2aaf2825f2018b Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 12:25:55 +0400 Subject: [PATCH 03/12] Remove scan triggers --- openframe-frontend-core/package.json | 1 - pom.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/openframe-frontend-core/package.json b/openframe-frontend-core/package.json index b426eb0445..da6525bfc2 100644 --- a/openframe-frontend-core/package.json +++ b/openframe-frontend-core/package.json @@ -532,4 +532,3 @@ ] } } - diff --git a/pom.xml b/pom.xml index 96b12d8b9a..c0d6c142e6 100644 --- a/pom.xml +++ b/pom.xml @@ -343,4 +343,3 @@ - From adcb00838bff96d9327e1a1d312f149758036c3f Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 12:41:15 +0400 Subject: [PATCH 04/12] Move scan gating into the trivy action --- .github/steps/trivy/action.yml | 39 ++++++++++++++++++++++++++++++---- .github/workflows/test.yml | 6 ++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index c37e9dda93..5ec51c5f05 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -16,14 +16,43 @@ inputs: runs: using: "composite" steps: + - name: Check if scan should run + id: should_run + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + SCAN: ${{ inputs.scan }} + SCAN_DIR: ${{ inputs.path }} + SKIP_DIRS: ${{ inputs.skip-dirs }} + run: | + set -euo pipefail + if [ "$SCAN" != "code" ] || [ -z "$PR_NUMBER" ]; then echo "run=true" >> "$GITHUB_OUTPUT"; exit 0; fi + gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' > /tmp/pr-files.txt + prefix="${SCAN_DIR#./}/"; [ "$prefix" = "./" ] && prefix="" + manifest='(^|/)(pom\.xml|package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|go\.mod|go\.sum|Cargo\.toml|Cargo\.lock|Dockerfile[^/]*)$' + always='^(pom\.xml|\.mvn/|\.trivyignore|\.github/steps/trivy/)' + skip="" + for dir in ${SKIP_DIRS//,/ }; do skip="${skip:+$skip|}^${prefix}${dir%/}/"; done + run=false + while read -r f; do + [[ "$f" =~ $always ]] && { run=true; break; } + [[ "$f" == "$prefix"* ]] || continue + [ -n "$skip" ] && [[ "$f" =~ $skip ]] && continue + [[ "$f" =~ $manifest ]] && { run=true; break; } + done < /tmp/pr-files.txt + echo "run=$run" >> "$GITHUB_OUTPUT" + [ "$run" = true ] || echo "No dependency manifests changed under $SCAN_DIR, scan skipped" + - name: Install Trivy + if: steps.should_run.outputs.run == 'true' uses: aquasecurity/setup-trivy@v0.3.1 with: version: v0.74.0 cache: true - name: Resolve Maven dependencies - if: inputs.scan == 'code' + if: inputs.scan == 'code' && steps.should_run.outputs.run == 'true' shell: bash env: GITHUB_TOKEN: ${{ inputs.maven-token }} @@ -40,7 +69,7 @@ runs: echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" - name: Scan dependencies - if: inputs.scan == 'code' + if: inputs.scan == 'code' && steps.should_run.outputs.run == 'true' shell: bash env: SCAN_DIR: ${{ inputs.path }} @@ -63,7 +92,7 @@ runs: done < "$report" > "$report.tmp" && mv "$report.tmp" "$report" - name: Scan Dockerfile base images - if: inputs.scan == 'code' + if: inputs.scan == 'code' && steps.should_run.outputs.run == 'true' shell: bash env: SCAN_DIR: ${{ inputs.path }} @@ -89,7 +118,7 @@ runs: find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Scan built image - if: inputs.scan == 'image' + if: inputs.scan == 'image' && steps.should_run.outputs.run == 'true' shell: bash env: GITHUB_TOKEN: ${{ inputs.maven-token }} @@ -105,6 +134,7 @@ runs: find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Upload report + if: steps.should_run.outputs.run == 'true' uses: actions/upload-artifact@v4 with: name: ${{ inputs.scan == 'code' && (inputs.image-name && format('scan_code-{0}', inputs.image-name) || 'scan_code') || format('scan_image-{0}', inputs.image-name) }} @@ -112,6 +142,7 @@ runs: if-no-files-found: ignore - name: Evaluate + if: steps.should_run.outputs.run == 'true' shell: bash env: ARTIFACT: ${{ inputs.scan == 'code' && 'scan_code' || 'scan_image' }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b9489f403..5635f1d945 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,8 @@ jobs: permissions: contents: read packages: read - if: fromJSON(needs.changes.outputs.changes || '{}').java == 'true' || fromJSON(needs.changes.outputs.changes || '{}').rust == 'true' + pull-requests: read + if: github.event_name == 'pull_request' steps: - name: Checkout uses: actions/checkout@v4 @@ -58,7 +59,8 @@ jobs: permissions: contents: read packages: read - if: fromJSON(needs.changes.outputs.changes || '{}')['openframe-frontend-core'] == 'true' + pull-requests: read + if: github.event_name == 'pull_request' steps: - name: Checkout uses: actions/checkout@v4 From dda7f4005ee08377c2c5e3fb0e528dc94852c03a Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 12:46:55 +0400 Subject: [PATCH 05/12] Move scan gating into the trivy action --- .github/steps/trivy/action.yml | 51 +++++++++++++--------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 5ec51c5f05..366d948931 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -18,41 +18,28 @@ runs: steps: - name: Check if scan should run id: should_run - shell: bash - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - SCAN: ${{ inputs.scan }} - SCAN_DIR: ${{ inputs.path }} - SKIP_DIRS: ${{ inputs.skip-dirs }} - run: | - set -euo pipefail - if [ "$SCAN" != "code" ] || [ -z "$PR_NUMBER" ]; then echo "run=true" >> "$GITHUB_OUTPUT"; exit 0; fi - gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' > /tmp/pr-files.txt - prefix="${SCAN_DIR#./}/"; [ "$prefix" = "./" ] && prefix="" - manifest='(^|/)(pom\.xml|package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|go\.mod|go\.sum|Cargo\.toml|Cargo\.lock|Dockerfile[^/]*)$' - always='^(pom\.xml|\.mvn/|\.trivyignore|\.github/steps/trivy/)' - skip="" - for dir in ${SKIP_DIRS//,/ }; do skip="${skip:+$skip|}^${prefix}${dir%/}/"; done - run=false - while read -r f; do - [[ "$f" =~ $always ]] && { run=true; break; } - [[ "$f" == "$prefix"* ]] || continue - [ -n "$skip" ] && [[ "$f" =~ $skip ]] && continue - [[ "$f" =~ $manifest ]] && { run=true; break; } - done < /tmp/pr-files.txt - echo "run=$run" >> "$GITHUB_OUTPUT" - [ "$run" = true ] || echo "No dependency manifests changed under $SCAN_DIR, scan skipped" + if: inputs.scan == 'code' + uses: dorny/paths-filter@v4.0.2 + with: + predicate-quantifier: some-with-excludes + filters: | + deps: + - 'pom.xml' + - '.mvn/**' + - '.trivyignore' + - '.github/steps/trivy/**' + - '${{ inputs.path }}/**/{pom.xml,package.json,package-lock.json,yarn.lock,pnpm-lock.yaml,go.mod,go.sum,Cargo.toml,Cargo.lock,Dockerfile*}' + ${{ inputs.skip-dirs && format('- ''!{0}/{1}/**''', inputs.path, inputs.skip-dirs) || '' }} - name: Install Trivy - if: steps.should_run.outputs.run == 'true' + if: steps.should_run.outputs.deps != 'false' uses: aquasecurity/setup-trivy@v0.3.1 with: version: v0.74.0 cache: true - name: Resolve Maven dependencies - if: inputs.scan == 'code' && steps.should_run.outputs.run == 'true' + if: inputs.scan == 'code' && steps.should_run.outputs.deps != 'false' shell: bash env: GITHUB_TOKEN: ${{ inputs.maven-token }} @@ -69,7 +56,7 @@ runs: echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" - name: Scan dependencies - if: inputs.scan == 'code' && steps.should_run.outputs.run == 'true' + if: inputs.scan == 'code' && steps.should_run.outputs.deps != 'false' shell: bash env: SCAN_DIR: ${{ inputs.path }} @@ -92,7 +79,7 @@ runs: done < "$report" > "$report.tmp" && mv "$report.tmp" "$report" - name: Scan Dockerfile base images - if: inputs.scan == 'code' && steps.should_run.outputs.run == 'true' + if: inputs.scan == 'code' && steps.should_run.outputs.deps != 'false' shell: bash env: SCAN_DIR: ${{ inputs.path }} @@ -118,7 +105,7 @@ runs: find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Scan built image - if: inputs.scan == 'image' && steps.should_run.outputs.run == 'true' + if: inputs.scan == 'image' shell: bash env: GITHUB_TOKEN: ${{ inputs.maven-token }} @@ -134,7 +121,7 @@ runs: find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Upload report - if: steps.should_run.outputs.run == 'true' + if: steps.should_run.outputs.deps != 'false' uses: actions/upload-artifact@v4 with: name: ${{ inputs.scan == 'code' && (inputs.image-name && format('scan_code-{0}', inputs.image-name) || 'scan_code') || format('scan_image-{0}', inputs.image-name) }} @@ -142,7 +129,7 @@ runs: if-no-files-found: ignore - name: Evaluate - if: steps.should_run.outputs.run == 'true' + if: steps.should_run.outputs.deps != 'false' shell: bash env: ARTIFACT: ${{ inputs.scan == 'code' && 'scan_code' || 'scan_image' }} From 65d02a656f85006d4312a847641a15664f09070a Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 12:55:57 +0400 Subject: [PATCH 06/12] Gate scan on changes under the scan path --- .github/steps/trivy/action.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 366d948931..a36514cdb2 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -19,27 +19,26 @@ runs: - name: Check if scan should run id: should_run if: inputs.scan == 'code' - uses: dorny/paths-filter@v4.0.2 + uses: dorny/paths-filter@v4.0.3 with: predicate-quantifier: some-with-excludes filters: | - deps: + changed: - 'pom.xml' - - '.mvn/**' - '.trivyignore' - '.github/steps/trivy/**' - - '${{ inputs.path }}/**/{pom.xml,package.json,package-lock.json,yarn.lock,pnpm-lock.yaml,go.mod,go.sum,Cargo.toml,Cargo.lock,Dockerfile*}' + - '${{ inputs.path }}/**' ${{ inputs.skip-dirs && format('- ''!{0}/{1}/**''', inputs.path, inputs.skip-dirs) || '' }} - name: Install Trivy - if: steps.should_run.outputs.deps != 'false' + if: steps.should_run.outputs.changed != 'false' uses: aquasecurity/setup-trivy@v0.3.1 with: version: v0.74.0 cache: true - name: Resolve Maven dependencies - if: inputs.scan == 'code' && steps.should_run.outputs.deps != 'false' + if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' shell: bash env: GITHUB_TOKEN: ${{ inputs.maven-token }} @@ -56,7 +55,7 @@ runs: echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" - name: Scan dependencies - if: inputs.scan == 'code' && steps.should_run.outputs.deps != 'false' + if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' shell: bash env: SCAN_DIR: ${{ inputs.path }} @@ -79,7 +78,7 @@ runs: done < "$report" > "$report.tmp" && mv "$report.tmp" "$report" - name: Scan Dockerfile base images - if: inputs.scan == 'code' && steps.should_run.outputs.deps != 'false' + if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' shell: bash env: SCAN_DIR: ${{ inputs.path }} @@ -121,7 +120,7 @@ runs: find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Upload report - if: steps.should_run.outputs.deps != 'false' + if: steps.should_run.outputs.changed != 'false' uses: actions/upload-artifact@v4 with: name: ${{ inputs.scan == 'code' && (inputs.image-name && format('scan_code-{0}', inputs.image-name) || 'scan_code') || format('scan_image-{0}', inputs.image-name) }} @@ -129,7 +128,7 @@ runs: if-no-files-found: ignore - name: Evaluate - if: steps.should_run.outputs.deps != 'false' + if: steps.should_run.outputs.changed != 'false' shell: bash env: ARTIFACT: ${{ inputs.scan == 'code' && 'scan_code' || 'scan_image' }} From 0365b6cda3c7e32f8a85a56442f1c8a39bd60667 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 13:31:05 +0400 Subject: [PATCH 07/12] Resolve Maven dependencies for nested modules under the scan path --- .github/steps/trivy/action.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index a36514cdb2..30c95dd355 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -47,10 +47,11 @@ runs: ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail - [ -f "$SCAN_DIR/pom.xml" ] || exit 0 - mvn_args=(-B -q -fn -DskipTests -gs "$ACTION_PATH/central-mirror.xml" -f "$SCAN_DIR/pom.xml") + poms=$([ -f "$SCAN_DIR/pom.xml" ] && echo "$SCAN_DIR/pom.xml" || find "$SCAN_DIR" -maxdepth 2 -name pom.xml) + [ -n "$poms" ] || exit 0 + mvn_args=(-B -q -fn -DskipTests -gs "$ACTION_PATH/central-mirror.xml") [ -f .mvn/settings.xml ] && mvn_args+=(-s .mvn/settings.xml) - mvn "${mvn_args[@]}" dependency:go-offline | tee /tmp/mvn.log || true + for pom in $poms; do mvn "${mvn_args[@]}" -f "$pom" dependency:go-offline | tee -a /tmp/mvn.log || true; done ! grep -q '\[ERROR\]' /tmp/mvn.log || echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" From dbba9c06d952e88a3a44eb363010c813295baf17 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 21:45:36 +0400 Subject: [PATCH 08/12] Run code scans as a matrix gated on dependency files and fail only on vulnerabilities new to the base branch --- .github/steps/trivy/action.yml | 81 +++++++++++++++++++++++++++------- .github/workflows/test.yml | 38 +++++----------- 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 30c95dd355..1fbb4128d0 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -27,8 +27,18 @@ runs: - 'pom.xml' - '.trivyignore' - '.github/steps/trivy/**' - - '${{ inputs.path }}/**' - ${{ inputs.skip-dirs && format('- ''!{0}/{1}/**''', inputs.path, inputs.skip-dirs) || '' }} + - '.mvn/**' + - '${{ inputs.path }}/**/pom.xml' + - '${{ inputs.path }}/**/package.json' + - '${{ inputs.path }}/**/package-lock.json' + - '${{ inputs.path }}/**/yarn.lock' + - '${{ inputs.path }}/**/pnpm-lock.yaml' + - '${{ inputs.path }}/**/go.mod' + - '${{ inputs.path }}/**/go.sum' + - '${{ inputs.path }}/**/Cargo.toml' + - '${{ inputs.path }}/**/Cargo.lock' + - '${{ inputs.path }}/**/Dockerfile*' + ${{ inputs.skip-dirs && format('- ''!{0}/{{{1},}}/**''', inputs.path, inputs.skip-dirs) || '' }} - name: Install Trivy if: steps.should_run.outputs.changed != 'false' @@ -37,6 +47,14 @@ runs: version: v0.74.0 cache: true + - name: Checkout base + if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: .trivy-base + persist-credentials: false + - name: Resolve Maven dependencies if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' shell: bash @@ -47,14 +65,33 @@ runs: ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail - poms=$([ -f "$SCAN_DIR/pom.xml" ] && echo "$SCAN_DIR/pom.xml" || find "$SCAN_DIR" -maxdepth 2 -name pom.xml) - [ -n "$poms" ] || exit 0 mvn_args=(-B -q -fn -DskipTests -gs "$ACTION_PATH/central-mirror.xml") [ -f .mvn/settings.xml ] && mvn_args+=(-s .mvn/settings.xml) - for pom in $poms; do mvn "${mvn_args[@]}" -f "$pom" dependency:go-offline | tee -a /tmp/mvn.log || true; done - ! grep -q '\[ERROR\]' /tmp/mvn.log || + for dir in "$SCAN_DIR" ".trivy-base/$SCAN_DIR"; do + [ -d "$dir" ] || continue + poms=$([ -f "$dir/pom.xml" ] && echo "$dir/pom.xml" || find "$dir" -maxdepth 2 -name pom.xml -not -path '*/.trivy-base/*') + for pom in $poms; do mvn "${mvn_args[@]}" -f "$pom" dependency:go-offline | tee -a /tmp/mvn.log || true; done + done + ! grep -qs '\[ERROR\]' /tmp/mvn.log || echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" + - name: Resolve npm dependencies + if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' + shell: bash + env: + SCAN_DIR: ${{ inputs.path }} + SKIP_DIRS: ${{ inputs.skip-dirs }} + run: | + set -euo pipefail + find_args=(-maxdepth 2 -name package.json -not -path '*/node_modules/*' -not -path '*/.trivy-base/*') + for dir in ${SKIP_DIRS//,/ }; do find_args+=(-not -path "*/$dir/*" -not -path "$dir/*"); done + for pkg in $(find "$SCAN_DIR" ".trivy-base/$SCAN_DIR" "${find_args[@]}" 2>/dev/null); do + dir=$(dirname "$pkg") + [ -e "$dir/package-lock.json" ] || [ -e "$dir/yarn.lock" ] || [ -e "$dir/pnpm-lock.yaml" ] || + npm install --package-lock-only --ignore-scripts --no-audit --no-fund --legacy-peer-deps --prefix "$dir" || + echo "::warning::npm dependencies in $dir could not be resolved; they were NOT scanned" + done + - name: Scan dependencies if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' shell: bash @@ -65,11 +102,13 @@ runs: ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail - args=(--no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json) + args=(--no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json --skip-dirs .trivy-base) [ -n "$SKIP_DIRS" ] && args+=(--skip-dirs "$SKIP_DIRS") [ -n "$SKIP_FILES" ] && args+=(--skip-files "$SKIP_FILES") report="trivy-${GITHUB_REPOSITORY##*/}-code-report.tsv" - trivy fs "${args[@]}" "$SCAN_DIR" | jq -r --arg src "" -f "$ACTION_PATH/to-tsv.jq" | sort -u > "$report" + scan() { trivy fs "${args[@]}" "$1" | jq -r --arg src "" -f "$ACTION_PATH/to-tsv.jq" | sort -u; } + scan "$SCAN_DIR" > "$report" + scan ".trivy-base/$SCAN_DIR" > /tmp/on-base.tsv || : > /tmp/on-base.tsv while IFS=$'\t' read -r severity pkg installed fixed cve target; do origin="-" if [[ "$target" == *pom.xml ]]; then @@ -77,6 +116,8 @@ runs: fi printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$severity" "$pkg" "$installed" "$fixed" "$cve" "$target" "$origin" done < "$report" > "$report.tmp" && mv "$report.tmp" "$report" + awk -F'\t' -v OFS='\t' 'FILENAME == ARGV[1] { seen[$2 FS $5]; next } { print $0, (($2 FS $5) in seen ? "existing" : "new") }' \ + /tmp/on-base.tsv "$report" > "$report.tmp" && mv "$report.tmp" "$report" - name: Scan Dockerfile base images if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' @@ -87,11 +128,15 @@ runs: ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail - find_args=(-name 'Dockerfile*' -not -path '*/.git/*' -not -path '*/node_modules/*') + find_args=(-name 'Dockerfile*' -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.trivy-base/*') for dir in ${SKIP_DIRS//,/ }; do find_args+=(-not -path "*/$dir/*" -not -path "$dir/*"); done - find "$SCAN_DIR" "${find_args[@]}" -print0 | - xargs -0 -r awk 'toupper($1)=="FROM" { img=$2; if (img ~ /^--/) img=$3; print img; if (toupper($3)=="AS") print "~"$4; if (toupper($4)=="AS") print "~"$5 }' | - sort -u > /tmp/base-images.txt + images() { + find "$1" "${find_args[@]}" -print0 2>/dev/null | + xargs -0 -r awk 'toupper($1)=="FROM" { img=$2; if (img ~ /^--/) img=$3; print img; if (toupper($3)=="AS") print "~"$4; if (toupper($4)=="AS") print "~"$5 }' | + sort -u + } + images "$SCAN_DIR" > /tmp/base-images.txt + images ".trivy-base/$SCAN_DIR" > /tmp/base-images-on-base.txt || : report="trivy-${GITHUB_REPOSITORY##*/}-docker-report.tsv" : > "$report" while read -r img; do @@ -101,7 +146,8 @@ runs: jq -r --arg src "$img" -f "$ACTION_PATH/to-tsv.jq" >> "$report" || echo "::warning::Base image $img could not be scanned (pull/scan error); it was NOT checked" done < /tmp/base-images.txt - sort -u "$report" -o "$report" + awk -F'\t' -v OFS='\t' 'FILENAME == ARGV[1] { seen[$1]; next } { print $0, ($6 in seen ? "existing" : "new") }' \ + /tmp/base-images-on-base.txt "$report" | sort -u > "$report.tmp" && mv "$report.tmp" "$report" find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Scan built image @@ -147,6 +193,11 @@ runs: files=$(ls trivy-*.tsv 2>/dev/null) || { echo "No vulnerabilities found."; report_status success "No HIGH/CRITICAL vulnerabilities found"; exit 0; } column -t -s "$(printf '\t')" $files unique=$(cut -f1,2,5 $files | sort -u | wc -l | tr -d ' ') - report_status failure "$unique HIGH/CRITICAL vulnerabilities — full report in the $ARTIFACT artifact" - echo "::error::Trivy found $unique unique HIGH/CRITICAL vulnerabilities ($(cat $files | wc -l | tr -d ' ') occurrences across modules) — full report in the ${ARTIFACT} artifact" + new=$(awk -F'\t' '$NF != "existing"' $files | cut -f1,2,5 | sort -u | wc -l | tr -d ' ') + if [ "$new" = 0 ]; then + echo "::warning::$unique HIGH/CRITICAL vulnerabilities already exist on the base branch; this PR adds none — full report in the ${ARTIFACT} artifact" + exit 0 + fi + report_status failure "$new new HIGH/CRITICAL vulnerabilities — full report in the $ARTIFACT artifact" + echo "::error::This PR adds $new HIGH/CRITICAL vulnerabilities ($unique in total, the rest already exist on the base branch) — full report in the ${ARTIFACT} artifact" exit 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5635f1d945..3faba0f0dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,31 +29,8 @@ jobs: if: github.event.pull_request.state == 'open' && !github.event.pull_request.draft - scan-backend: - name: "Scan Code: backend" - runs-on: ubuntu-latest - needs: [changes] - permissions: - contents: read - packages: read - pull-requests: read - if: github.event_name == 'pull_request' - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false - - - name: Scan code - uses: ./.github/steps/trivy - with: - scan: code - skip-dirs: openframe-frontend-core - image-name: backend - - scan-frontend: - name: "Scan Code: frontend" + scan: + name: "Scan Code: ${{ matrix.target.name }}" runs-on: ubuntu-latest needs: [changes] permissions: @@ -61,6 +38,12 @@ jobs: packages: read pull-requests: read if: github.event_name == 'pull_request' + strategy: + fail-fast: false + matrix: + target: + - { name: backend, path: ".", skip: "openframe-frontend-core,react-embedding-example" } + - { name: frontend, path: openframe-frontend-core } steps: - name: Checkout uses: actions/checkout@v4 @@ -72,8 +55,9 @@ jobs: uses: ./.github/steps/trivy with: scan: code - path: openframe-frontend-core - image-name: frontend + path: ${{ matrix.target.path }} + skip-dirs: ${{ matrix.target.skip || '' }} + image-name: ${{ matrix.target.name }} test-java: name: Test Java From a0efd94c67ad88b20da763d60595bf0a4cb33bac Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 21:57:36 +0400 Subject: [PATCH 09/12] Drop base comparison and npm resolution, gate scans on listed paths --- .github/steps/trivy/action.yml | 82 +++++++--------------------------- .github/workflows/test.yml | 2 +- 2 files changed, 17 insertions(+), 67 deletions(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 1fbb4128d0..937db802e0 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -6,6 +6,7 @@ inputs: description: "'code' or 'image'" required: true path: { default: "." } + paths: { default: "" } skip-dirs: { default: "" } skip-files: { default: "" } image-name: { default: "" } @@ -27,18 +28,8 @@ runs: - 'pom.xml' - '.trivyignore' - '.github/steps/trivy/**' - - '.mvn/**' - - '${{ inputs.path }}/**/pom.xml' - - '${{ inputs.path }}/**/package.json' - - '${{ inputs.path }}/**/package-lock.json' - - '${{ inputs.path }}/**/yarn.lock' - - '${{ inputs.path }}/**/pnpm-lock.yaml' - - '${{ inputs.path }}/**/go.mod' - - '${{ inputs.path }}/**/go.sum' - - '${{ inputs.path }}/**/Cargo.toml' - - '${{ inputs.path }}/**/Cargo.lock' - - '${{ inputs.path }}/**/Dockerfile*' - ${{ inputs.skip-dirs && format('- ''!{0}/{{{1},}}/**''', inputs.path, inputs.skip-dirs) || '' }} + - '${{ inputs.paths && format('{{{0},}}', inputs.paths) || format('{0}/**', inputs.path) }}' + ${{ !inputs.paths && inputs.skip-dirs && format('- ''!{0}/{1}/**''', inputs.path, inputs.skip-dirs) || '' }} - name: Install Trivy if: steps.should_run.outputs.changed != 'false' @@ -47,14 +38,6 @@ runs: version: v0.74.0 cache: true - - name: Checkout base - if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.base.sha }} - path: .trivy-base - persist-credentials: false - - name: Resolve Maven dependencies if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' shell: bash @@ -65,33 +48,14 @@ runs: ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail + poms=$([ -f "$SCAN_DIR/pom.xml" ] && echo "$SCAN_DIR/pom.xml" || find "$SCAN_DIR" -maxdepth 2 -name pom.xml) + [ -n "$poms" ] || exit 0 mvn_args=(-B -q -fn -DskipTests -gs "$ACTION_PATH/central-mirror.xml") [ -f .mvn/settings.xml ] && mvn_args+=(-s .mvn/settings.xml) - for dir in "$SCAN_DIR" ".trivy-base/$SCAN_DIR"; do - [ -d "$dir" ] || continue - poms=$([ -f "$dir/pom.xml" ] && echo "$dir/pom.xml" || find "$dir" -maxdepth 2 -name pom.xml -not -path '*/.trivy-base/*') - for pom in $poms; do mvn "${mvn_args[@]}" -f "$pom" dependency:go-offline | tee -a /tmp/mvn.log || true; done - done - ! grep -qs '\[ERROR\]' /tmp/mvn.log || + for pom in $poms; do mvn "${mvn_args[@]}" -f "$pom" dependency:go-offline | tee -a /tmp/mvn.log || true; done + ! grep -q '\[ERROR\]' /tmp/mvn.log || echo "::warning::Some Maven dependencies could not be resolved; scan depth may be reduced" - - name: Resolve npm dependencies - if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' - shell: bash - env: - SCAN_DIR: ${{ inputs.path }} - SKIP_DIRS: ${{ inputs.skip-dirs }} - run: | - set -euo pipefail - find_args=(-maxdepth 2 -name package.json -not -path '*/node_modules/*' -not -path '*/.trivy-base/*') - for dir in ${SKIP_DIRS//,/ }; do find_args+=(-not -path "*/$dir/*" -not -path "$dir/*"); done - for pkg in $(find "$SCAN_DIR" ".trivy-base/$SCAN_DIR" "${find_args[@]}" 2>/dev/null); do - dir=$(dirname "$pkg") - [ -e "$dir/package-lock.json" ] || [ -e "$dir/yarn.lock" ] || [ -e "$dir/pnpm-lock.yaml" ] || - npm install --package-lock-only --ignore-scripts --no-audit --no-fund --legacy-peer-deps --prefix "$dir" || - echo "::warning::npm dependencies in $dir could not be resolved; they were NOT scanned" - done - - name: Scan dependencies if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' shell: bash @@ -102,13 +66,11 @@ runs: ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail - args=(--no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json --skip-dirs .trivy-base) + args=(--no-progress --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --offline-scan --format json) [ -n "$SKIP_DIRS" ] && args+=(--skip-dirs "$SKIP_DIRS") [ -n "$SKIP_FILES" ] && args+=(--skip-files "$SKIP_FILES") report="trivy-${GITHUB_REPOSITORY##*/}-code-report.tsv" - scan() { trivy fs "${args[@]}" "$1" | jq -r --arg src "" -f "$ACTION_PATH/to-tsv.jq" | sort -u; } - scan "$SCAN_DIR" > "$report" - scan ".trivy-base/$SCAN_DIR" > /tmp/on-base.tsv || : > /tmp/on-base.tsv + trivy fs "${args[@]}" "$SCAN_DIR" | jq -r --arg src "" -f "$ACTION_PATH/to-tsv.jq" | sort -u > "$report" while IFS=$'\t' read -r severity pkg installed fixed cve target; do origin="-" if [[ "$target" == *pom.xml ]]; then @@ -116,8 +78,6 @@ runs: fi printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$severity" "$pkg" "$installed" "$fixed" "$cve" "$target" "$origin" done < "$report" > "$report.tmp" && mv "$report.tmp" "$report" - awk -F'\t' -v OFS='\t' 'FILENAME == ARGV[1] { seen[$2 FS $5]; next } { print $0, (($2 FS $5) in seen ? "existing" : "new") }' \ - /tmp/on-base.tsv "$report" > "$report.tmp" && mv "$report.tmp" "$report" - name: Scan Dockerfile base images if: inputs.scan == 'code' && steps.should_run.outputs.changed != 'false' @@ -128,15 +88,11 @@ runs: ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail - find_args=(-name 'Dockerfile*' -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.trivy-base/*') + find_args=(-name 'Dockerfile*' -not -path '*/.git/*' -not -path '*/node_modules/*') for dir in ${SKIP_DIRS//,/ }; do find_args+=(-not -path "*/$dir/*" -not -path "$dir/*"); done - images() { - find "$1" "${find_args[@]}" -print0 2>/dev/null | - xargs -0 -r awk 'toupper($1)=="FROM" { img=$2; if (img ~ /^--/) img=$3; print img; if (toupper($3)=="AS") print "~"$4; if (toupper($4)=="AS") print "~"$5 }' | - sort -u - } - images "$SCAN_DIR" > /tmp/base-images.txt - images ".trivy-base/$SCAN_DIR" > /tmp/base-images-on-base.txt || : + find "$SCAN_DIR" "${find_args[@]}" -print0 | + xargs -0 -r awk 'toupper($1)=="FROM" { img=$2; if (img ~ /^--/) img=$3; print img; if (toupper($3)=="AS") print "~"$4; if (toupper($4)=="AS") print "~"$5 }' | + sort -u > /tmp/base-images.txt report="trivy-${GITHUB_REPOSITORY##*/}-docker-report.tsv" : > "$report" while read -r img; do @@ -146,8 +102,7 @@ runs: jq -r --arg src "$img" -f "$ACTION_PATH/to-tsv.jq" >> "$report" || echo "::warning::Base image $img could not be scanned (pull/scan error); it was NOT checked" done < /tmp/base-images.txt - awk -F'\t' -v OFS='\t' 'FILENAME == ARGV[1] { seen[$1]; next } { print $0, ($6 in seen ? "existing" : "new") }' \ - /tmp/base-images-on-base.txt "$report" | sort -u > "$report.tmp" && mv "$report.tmp" "$report" + sort -u "$report" -o "$report" find . -maxdepth 1 -name 'trivy-*.tsv' -empty -delete - name: Scan built image @@ -193,11 +148,6 @@ runs: files=$(ls trivy-*.tsv 2>/dev/null) || { echo "No vulnerabilities found."; report_status success "No HIGH/CRITICAL vulnerabilities found"; exit 0; } column -t -s "$(printf '\t')" $files unique=$(cut -f1,2,5 $files | sort -u | wc -l | tr -d ' ') - new=$(awk -F'\t' '$NF != "existing"' $files | cut -f1,2,5 | sort -u | wc -l | tr -d ' ') - if [ "$new" = 0 ]; then - echo "::warning::$unique HIGH/CRITICAL vulnerabilities already exist on the base branch; this PR adds none — full report in the ${ARTIFACT} artifact" - exit 0 - fi - report_status failure "$new new HIGH/CRITICAL vulnerabilities — full report in the $ARTIFACT artifact" - echo "::error::This PR adds $new HIGH/CRITICAL vulnerabilities ($unique in total, the rest already exist on the base branch) — full report in the ${ARTIFACT} artifact" + report_status failure "$unique HIGH/CRITICAL vulnerabilities — full report in the $ARTIFACT artifact" + echo "::error::Trivy found $unique unique HIGH/CRITICAL vulnerabilities ($(cat $files | wc -l | tr -d ' ') occurrences across modules) — full report in the ${ARTIFACT} artifact" exit 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3faba0f0dc..2e8ec3c220 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: fail-fast: false matrix: target: - - { name: backend, path: ".", skip: "openframe-frontend-core,react-embedding-example" } + - { name: backend, path: ".", skip: openframe-frontend-core } - { name: frontend, path: openframe-frontend-core } steps: - name: Checkout From 0ddf4a2ffb57e54dbce20f981c35e8e5fa8a11d9 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 22:23:36 +0400 Subject: [PATCH 10/12] Split root scans into backend and frontend targets --- .github/steps/trivy/action.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 937db802e0..36633a113c 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -28,7 +28,7 @@ runs: - 'pom.xml' - '.trivyignore' - '.github/steps/trivy/**' - - '${{ inputs.paths && format('{{{0},}}', inputs.paths) || format('{0}/**', inputs.path) }}' + - '${{ inputs.paths || format('{0}/**', inputs.path) }}' ${{ !inputs.paths && inputs.skip-dirs && format('- ''!{0}/{1}/**''', inputs.path, inputs.skip-dirs) || '' }} - name: Install Trivy @@ -85,11 +85,14 @@ runs: env: SCAN_DIR: ${{ inputs.path }} SKIP_DIRS: ${{ inputs.skip-dirs }} + SKIP_FILES: ${{ inputs.skip-files }} ACTION_PATH: ${{ github.action_path }} run: | set -euo pipefail find_args=(-name 'Dockerfile*' -not -path '*/.git/*' -not -path '*/node_modules/*') for dir in ${SKIP_DIRS//,/ }; do find_args+=(-not -path "*/$dir/*" -not -path "$dir/*"); done + IFS=, read -ra skip_files <<< "$SKIP_FILES" + for file in "${skip_files[@]}"; do find_args+=(-not -name "${file##*/}"); done find "$SCAN_DIR" "${find_args[@]}" -print0 | xargs -0 -r awk 'toupper($1)=="FROM" { img=$2; if (img ~ /^--/) img=$3; print img; if (toupper($3)=="AS") print "~"$4; if (toupper($4)=="AS") print "~"$5 }' | sort -u > /tmp/base-images.txt From 5ed613f9780fbdde95afeb89f784be384e9e893f Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Mon, 28 Sep 2026 23:10:24 +0400 Subject: [PATCH 11/12] Run code scan only when dependency files change --- .github/steps/trivy/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/steps/trivy/action.yml b/.github/steps/trivy/action.yml index 36633a113c..d32dfeaad9 100644 --- a/.github/steps/trivy/action.yml +++ b/.github/steps/trivy/action.yml @@ -28,7 +28,7 @@ runs: - 'pom.xml' - '.trivyignore' - '.github/steps/trivy/**' - - '${{ inputs.paths || format('{0}/**', inputs.path) }}' + - '${{ inputs.paths || format('{0}/**/{{pom.xml,package.json,package-lock.json,yarn.lock,pnpm-lock.yaml,go.mod,go.sum,Cargo.toml,Cargo.lock,Dockerfile*}}', inputs.path) }}' ${{ !inputs.paths && inputs.skip-dirs && format('- ''!{0}/{1}/**''', inputs.path, inputs.skip-dirs) || '' }} - name: Install Trivy From ff285e6efe18efc8eb4080d67e6d513d537726e6 Mon Sep 17 00:00:00 2001 From: yaroslavmokflmg Date: Tue, 29 Sep 2026 15:10:01 +0400 Subject: [PATCH 12/12] Take frontend scan targets from the changes node matrix --- .github/workflows/test.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e8ec3c220..2266be0983 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,9 +41,9 @@ jobs: strategy: fail-fast: false matrix: - target: - - { name: backend, path: ".", skip: openframe-frontend-core } - - { name: frontend, path: openframe-frontend-core } + target: ${{ fromJson(needs.changes.outputs.node_matrix) }} + include: + - target: { name: backend, path: ".", paths: "**/{pom.xml,Cargo.toml,Cargo.lock,Dockerfile*}", skip_files: "**/package.json,**/package-lock.json,**/yarn.lock,**/pnpm-lock.yaml" } steps: - name: Checkout uses: actions/checkout@v4 @@ -56,7 +56,8 @@ jobs: with: scan: code path: ${{ matrix.target.path }} - skip-dirs: ${{ matrix.target.skip || '' }} + paths: ${{ matrix.target.paths || '' }} + skip-files: ${{ matrix.target.skip_files || '' }} image-name: ${{ matrix.target.name }} test-java: