Skip to content

Commit cb64063

Browse files
Standardize bandit workflow to match pytest patterns (#42)
- Update action versions (checkout@v4.2.2, setup-python@v5.3.0) - Add consistent inputs: ref, python-version, artifact_name, severity_level - Rename target_branch_to_compare to target_branch for consistency - Add submodules checkout and standardize checkout configuration - Expand outputs: pr_issues_count, target_issues_count, new_issues_count, resolved_issues_count, has_regressions - Add artifact retention-days (3) and if-no-files-found settings - Improve regression detection with detailed issue reporting - Add resolved issues tracking and notification - Upload comparison artifacts for debugging Co-authored-by: Claude <noreply@anthropic.com>
1 parent 19e6746 commit cb64063

1 file changed

Lines changed: 156 additions & 66 deletions

File tree

Lines changed: 156 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,220 @@
11
name: Reusable Bandit Security Check with Regression Detection
22

3-
# This reusable workflow is triggered by other workflows using 'workflow_call'
43
on:
54
workflow_call:
65
inputs:
7-
target_branch_to_compare:
8-
description: "Target branch to compare against (e.g., main)"
6+
ref:
7+
description: "Git ref to checkout and test. Leave empty for default checkout."
8+
required: false
9+
type: string
10+
default: ""
11+
target_branch:
12+
description: "Target branch to compare against for regression detection (e.g., main)"
913
required: true
1014
type: string
15+
python-version:
16+
description: "Python version to use for Bandit."
17+
required: false
18+
type: string
19+
default: "3.10"
1120
runs_on:
21+
description: "Runner label for the jobs."
1222
required: false
1323
type: string
1424
default: '["ubuntu-latest"]'
25+
artifact_name:
26+
description: "Base name for the security scan artifacts."
27+
required: false
28+
type: string
29+
default: "bandit-results"
30+
severity_level:
31+
description: "Minimum severity level to report (-l, -ll, or -lll). Default -lll (high only)."
32+
required: false
33+
type: string
34+
default: "-lll"
1535
outputs:
36+
pr_issues_count:
37+
description: "Number of issues found on PR branch"
38+
value: ${{ jobs.run-bandit-pr.outputs.issues_count }}
39+
target_issues_count:
40+
description: "Number of issues found on target branch"
41+
value: ${{ jobs.run-bandit-target.outputs.issues_count }}
42+
new_issues_count:
43+
description: "Number of new issues introduced in PR"
44+
value: ${{ jobs.compare-results.outputs.new_issues_count }}
45+
resolved_issues_count:
46+
description: "Number of issues resolved in PR"
47+
value: ${{ jobs.compare-results.outputs.resolved_issues_count }}
48+
has_regressions:
49+
description: "Whether new security issues were introduced"
50+
value: ${{ jobs.compare-results.outputs.has_regressions }}
1651
bandit_issues_json:
1752
description: "JSON output of Bandit issues on PR branch"
18-
value: ${{ jobs.run-bandit.outputs.bandit_issues_json }}
53+
value: ${{ jobs.run-bandit-pr.outputs.bandit_issues_json }}
1954

2055
jobs:
2156
# Job 1: Run Bandit on the PR branch
22-
run-bandit:
23-
name: Run Bandit on PR Branch & Extract Results
57+
run-bandit-pr:
58+
name: Run Bandit on PR Branch
2459
runs-on: ${{ fromJSON(inputs.runs_on) }}
2560
outputs:
26-
bandit_issues_json: ${{ steps.extract-pr.outputs.BANDIT_JSON }}
61+
bandit_issues_json: ${{ steps.extract-results.outputs.bandit_json }}
62+
issues_count: ${{ steps.extract-results.outputs.issues_count }}
2763
steps:
28-
# Step 1: Checkout the current pull request code
2964
- name: Checkout PR Branch
30-
uses: actions/checkout@v4.1.1
65+
uses: actions/checkout@v4.2.2
3166
with:
67+
submodules: "recursive"
68+
ref: ${{ inputs.ref || github.ref }}
3269
persist-credentials: false
3370

34-
# Step 2: Set up Python 3.10 environment
3571
- name: Set up Python
36-
uses: actions/setup-python@v5
72+
uses: actions/setup-python@v5.3.0
3773
with:
38-
python-version: "3.10"
74+
python-version: "${{ inputs.python-version }}"
3975

40-
# Step 3: Install Bandit (Python security scanner)
4176
- name: Install Bandit
42-
run: pip install bandit
77+
run: |
78+
python -m pip install --upgrade pip
79+
pip install bandit
80+
81+
- name: Run Bandit Security Scan
82+
run: |
83+
bandit -r . ${{ inputs.severity_level }} -f json -o bandit_output.json || true
4384
44-
# Step 4: Run Bandit and output results to a file
45-
- name: Run Bandit on PR Branch
85+
- name: Extract Results
86+
id: extract-results
4687
run: |
47-
bandit -r . -lll -f json -o pr_bandit_output.json || true
88+
if [ -f bandit_output.json ]; then
89+
ISSUES_JSON=$(cat bandit_output.json | jq -c '.results')
90+
ISSUES_COUNT=$(cat bandit_output.json | jq '.results | length')
91+
else
92+
ISSUES_JSON="[]"
93+
ISSUES_COUNT=0
94+
fi
95+
echo "bandit_json=$ISSUES_JSON" >> "$GITHUB_OUTPUT"
96+
echo "issues_count=$ISSUES_COUNT" >> "$GITHUB_OUTPUT"
97+
echo "Found $ISSUES_COUNT security issues on PR branch"
4898
49-
# Step 5: Upload the results as a GitHub Actions artifact (for debugging or reporting)
50-
- name: Upload PR Artifact
99+
- name: Upload PR Branch Artifacts
51100
uses: actions/upload-artifact@v4
52101
with:
53-
name: pr_bandit_output
54-
path: pr_bandit_output.json
55-
56-
# Step 6: Extract the raw issue list from the Bandit JSON output
57-
- name: Extract PR Bandit JSON
58-
id: extract-pr
59-
run: |
60-
CONTENT=$(cat pr_bandit_output.json | jq -c '.results')
61-
echo "BANDIT_JSON=$CONTENT" >> $GITHUB_OUTPUT
102+
name: ${{ inputs.artifact_name }}-pr
103+
path: bandit_output.json
104+
retention-days: 3
105+
if-no-files-found: ignore
62106

63107
# Job 2: Run Bandit on the target branch for comparison
64-
run-bandit-on-target:
108+
run-bandit-target:
65109
name: Run Bandit on Target Branch
66110
runs-on: ${{ fromJSON(inputs.runs_on) }}
67111
outputs:
68-
bandit_target_json: ${{ steps.extract-target.outputs.TARGET_JSON }}
112+
bandit_issues_json: ${{ steps.extract-results.outputs.bandit_json }}
113+
issues_count: ${{ steps.extract-results.outputs.issues_count }}
69114
steps:
70-
# Step 1: Checkout the base branch (e.g., main)
71115
- name: Checkout Target Branch
72-
uses: actions/checkout@v4
116+
uses: actions/checkout@v4.2.2
73117
with:
74-
ref: ${{ inputs.target_branch_to_compare }}
118+
submodules: "recursive"
119+
ref: ${{ inputs.target_branch }}
75120
persist-credentials: false
76121

77-
# Step 2: Set up Python environment
78122
- name: Set up Python
79-
uses: actions/setup-python@v5
123+
uses: actions/setup-python@v5.3.0
80124
with:
81-
python-version: "3.10"
125+
python-version: "${{ inputs.python-version }}"
82126

83-
# Step 3: Install Bandit
84127
- name: Install Bandit
85-
run: pip install bandit
86-
87-
# Step 4: Run Bandit and save output
88-
- name: Run Bandit on Target Branch
89128
run: |
90-
bandit -r . -lll -f json -o target_bandit_output.json || true
129+
python -m pip install --upgrade pip
130+
pip install bandit
91131
92-
# Step 5: Upload results from the target branch
93-
- name: Upload Target Artifact
94-
uses: actions/upload-artifact@v4
95-
with:
96-
name: target_bandit_output
97-
path: target_bandit_output.json
132+
- name: Run Bandit Security Scan
133+
run: |
134+
bandit -r . ${{ inputs.severity_level }} -f json -o bandit_output.json || true
98135
99-
# Step 6: Extract raw issue list from the Bandit output
100-
- name: Extract Target Bandit JSON
101-
id: extract-target
136+
- name: Extract Results
137+
id: extract-results
102138
run: |
103-
CONTENT=$(cat target_bandit_output.json | jq -c '.results')
104-
echo "TARGET_JSON=$CONTENT" >> $GITHUB_OUTPUT
139+
if [ -f bandit_output.json ]; then
140+
ISSUES_JSON=$(cat bandit_output.json | jq -c '.results')
141+
ISSUES_COUNT=$(cat bandit_output.json | jq '.results | length')
142+
else
143+
ISSUES_JSON="[]"
144+
ISSUES_COUNT=0
145+
fi
146+
echo "bandit_json=$ISSUES_JSON" >> "$GITHUB_OUTPUT"
147+
echo "issues_count=$ISSUES_COUNT" >> "$GITHUB_OUTPUT"
148+
echo "Found $ISSUES_COUNT security issues on target branch"
105149
106-
# Job 3: Compare the PR results against the target to detect regressions
107-
compare-bandit:
108-
name: Compare Bandit Issues (Regression Analysis)
150+
- name: Upload Target Branch Artifacts
151+
uses: actions/upload-artifact@v4
152+
with:
153+
name: ${{ inputs.artifact_name }}-target
154+
path: bandit_output.json
155+
retention-days: 3
156+
if-no-files-found: ignore
157+
158+
# Job 3: Compare results and detect regressions
159+
compare-results:
160+
name: Compare Results (Regression Detection)
109161
runs-on: ${{ fromJSON(inputs.runs_on) }}
110-
needs: [run-bandit, run-bandit-on-target]
162+
needs: [run-bandit-pr, run-bandit-target]
163+
outputs:
164+
new_issues_count: ${{ steps.compare.outputs.new_issues_count }}
165+
resolved_issues_count: ${{ steps.compare.outputs.resolved_issues_count }}
166+
has_regressions: ${{ steps.compare.outputs.has_regressions }}
111167
steps:
112-
- name: Compare JSON
168+
- name: Compare Bandit Results
169+
id: compare
113170
run: |
114171
echo "Comparing Bandit results between PR and target branch..."
115172
116-
echo "${{ needs.run-bandit.outputs.bandit_issues_json }}" > pr.json
117-
echo "${{ needs.run-bandit-on-target.outputs.bandit_target_json }}" > target.json
173+
# Write issues to files for comparison
174+
echo '${{ needs.run-bandit-pr.outputs.bandit_issues_json }}' > pr_issues.json
175+
echo '${{ needs.run-bandit-target.outputs.bandit_issues_json }}' > target_issues.json
118176
119-
# Compare both JSON lists to find issues present in PR but not in target
120-
NEW_ISSUES=$(jq -n --argfile pr pr.json --argfile base target.json '
121-
$pr - $base | length')
177+
# Calculate new issues (in PR but not in target)
178+
NEW_ISSUES_COUNT=$(jq -n --argfile pr pr_issues.json --argfile base target_issues.json '
179+
($pr - $base) | length')
122180
123-
echo "New security issues introduced: $NEW_ISSUES"
181+
# Calculate resolved issues (in target but not in PR)
182+
RESOLVED_ISSUES_COUNT=$(jq -n --argfile pr pr_issues.json --argfile base target_issues.json '
183+
($base - $pr) | length')
184+
185+
echo "new_issues_count=$NEW_ISSUES_COUNT" >> "$GITHUB_OUTPUT"
186+
echo "resolved_issues_count=$RESOLVED_ISSUES_COUNT" >> "$GITHUB_OUTPUT"
187+
188+
echo "PR Issues: ${{ needs.run-bandit-pr.outputs.issues_count }}"
189+
echo "Target Issues: ${{ needs.run-bandit-target.outputs.issues_count }}"
190+
echo "New Issues Introduced: $NEW_ISSUES_COUNT"
191+
echo "Issues Resolved: $RESOLVED_ISSUES_COUNT"
192+
193+
if [ "$NEW_ISSUES_COUNT" -gt 0 ]; then
194+
echo "has_regressions=true" >> "$GITHUB_OUTPUT"
195+
echo "::error::$NEW_ISSUES_COUNT new security issue(s) introduced in this PR"
196+
197+
# Show details of new issues
198+
echo "New issues details:"
199+
jq -n --argfile pr pr_issues.json --argfile base target_issues.json '
200+
$pr - $base' | jq -r '.[] | " - \(.test_id): \(.issue_text) (\(.filename):\(.line_number))"'
124201
125-
if [ "$NEW_ISSUES" -gt 0 ]; then
126-
echo "::error::New Bandit issues introduced in PR branch."
127202
exit 1
128203
else
204+
echo "has_regressions=false" >> "$GITHUB_OUTPUT"
129205
echo "No new security issues introduced."
206+
if [ "$RESOLVED_ISSUES_COUNT" -gt 0 ]; then
207+
echo "::notice::$RESOLVED_ISSUES_COUNT security issue(s) were resolved in this PR"
208+
fi
130209
fi
210+
211+
- name: Upload Comparison Artifacts
212+
if: always()
213+
uses: actions/upload-artifact@v4
214+
with:
215+
name: ${{ inputs.artifact_name }}-comparison
216+
path: |
217+
pr_issues.json
218+
target_issues.json
219+
retention-days: 3
220+
if-no-files-found: ignore

0 commit comments

Comments
 (0)