forked from strands-agents/samples
-
Notifications
You must be signed in to change notification settings - Fork 0
294 lines (247 loc) · 10.6 KB
/
ash-pr-security-scan.yml
File metadata and controls
294 lines (247 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
name: ASH PR Scan
# Trigger: Run this workflow when PRs are opened/updated against main branch
on:
pull_request:
branches: [ main ]
paths-ignore:
- '**/*.md'
- '.github/**'
permissions:
contents: read
concurrency:
group: ash-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
ASH_VERSION: v3.0.0
PYTHON_VERSION: '3.11'
FAIL_ON_SEVERITY: 'high' # none|low|medium|high|critical
jobs:
pr-scan:
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install ASH
run: |
python -m pip install --upgrade pip
pip install "git+https://github.com/awslabs/automated-security-helper.git@${ASH_VERSION}"
- name: Write ASH config
run: |
cat > .ash_config.yaml << 'EOF'
reporters:
json:
enabled: true
options:
output_path: .ash/ash_output/reports/ash.summary.json
markdown:
enabled: true
options:
include_detailed_findings: true
max_detailed_findings: 500
EOF
- name: Get PR changed files
if: always()
id: changed-files
uses: tj-actions/changed-files@v46
- name: Create PR files directory for scanning
if: always()
run: |
mkdir -p pr-scan-dir
# Save changed files list for Python script
changed_files="${{ steps.changed-files.outputs.all_changed_files }}"
if [ -n "$changed_files" ]; then
echo "$changed_files" | tr ' ' '\n' > changed-files.txt
echo "Changed files:"
cat changed-files.txt
# Copy only changed files to scan directory
for file in $changed_files; do
if [ -f "$file" ]; then
# Create directory structure
mkdir -p "pr-scan-dir/$(dirname "$file")"
# Copy the file
cp "$file" "pr-scan-dir/$file"
echo "Copied: $file"
fi
done
else
echo "No files changed in this PR"
touch changed-files.txt
fi
echo "Files to scan:"
find pr-scan-dir -type f 2>/dev/null | wc -l
- name: Run ASH
run: |
set -o pipefail
file_count=$(find pr-scan-dir -type f 2>/dev/null | wc -l)
if [ "$file_count" -gt 0 ]; then
echo "Scanning $file_count files in PR..."
cd pr-scan-dir # Scan only the PR files directory
ash --mode container --config ../.ash_config.yaml 2>&1 | tee ../ash-output.log || true
else
echo "No files to scan, skipping ASH"
mkdir -p .ash/ash_output/reports
echo '{"runs": [{"results": []}]}' > .ash/ash_output/reports/ash.sarif
echo "No files changed - skipping security scan" > ash-output.log
fi
- name: Debug ASH Output Structure
if: always()
run: |
echo "=== ASH Directory Structure ==="
find .ash -type f -name "*.sarif" -o -name "*.json" -o -name "*.md" | head -20
echo -e "\n=== SARIF File Sample (first 100 lines) ==="
if [ -f "pr-scan-dir/.ash/ash_output/reports/ash.sarif" ]; then
head -100 pr-scan-dir/.ash/ash_output/reports/ash.sarif
else
echo "SARIF file not found!"
fi
echo -e "\n=== Files in reports directory ==="
ls -la pr-scan-dir/.ash/ash_output/reports/ || echo "Reports directory not found"
- name: Summarize & decide pass/fail
if: always()
id: summarize
run: |
python - << 'PY'
import json, os, pathlib, sys, re
# Read changed files for display
changed_files = []
if os.path.exists("changed-files.txt"):
with open("changed-files.txt", "r") as f:
changed_files = [line.strip() for line in f if line.strip()]
# Read ASH results - since we only scanned PR files, all findings are PR-related
sarif_path = pathlib.Path("pr-scan-dir/.ash/ash_output/reports/ash.sarif")
agg_path = pathlib.Path("pr-scan-dir/.ash/ash_output/ash_aggregated_results.json")
sev = dict(critical=0, high=0, medium=0, low=0, info=0)
findings = []
# Try to read SARIF file first (more detailed)
if sarif_path.exists():
try:
with open(sarif_path) as f:
sarif = json.load(f)
except json.JSONDecodeError as e:
print(f"Warning: Could not parse SARIF file: {e}")
sarif = {"runs": []} # Use empty default
for run in sarif.get("runs", []):
for result in run.get("results", []):
severity = result.get("level", "info").lower()
if severity == "warning": severity = "medium"
elif severity == "error": severity = "high"
sev[severity] = sev.get(severity, 0) + 1
# Get file path and details for high/critical findings
if severity in ["critical", "high"]:
file_path = "unknown"
line_num = 1
for location in result.get("locations", []):
uri = location.get("physicalLocation", {}).get("artifactLocation", {}).get("uri", "")
if uri:
file_path = uri.lstrip("./")
line_num = location.get("physicalLocation", {}).get("region", {}).get("startLine", 1)
break
rule_id = result.get("ruleId", "")
message = result.get("message", {}).get("text", "Security issue detected")
findings.append({
"file": file_path,
"line": line_num,
"severity": severity,
"rule": rule_id,
"message": message
})
# Fallback to aggregated results if no SARIF
elif agg_path.exists():
try:
with open(agg_path) as f:
d = json.load(f)
totals = d.get("totals", {})
for k in sev: sev[k] = int(totals.get(k, 0))
except (json.JSONDecodeError, ValueError) as e:
print(f"Warning: Could not parse aggregated results file: {e}")
# Keep default sev values (all zeros)
# Determine failure based on threshold
order = ["critical","high","medium","low","info"]
idx = {k:i for i,k in enumerate(order)}
threshold = os.getenv("FAIL_ON_SEVERITY","none").lower()
fail = any(sev[k] > 0 for k in order if idx[k] <= idx.get(threshold, 99))
# Generate markdown report
with open("ash-summary.md","w") as f:
# Determine status icon based on findings
if sev['critical'] > 0 or sev['high'] > 0:
status_icon = "❌"
elif sev['medium'] > 0 or sev['low'] > 0:
status_icon = "⚠️"
else:
status_icon = "✅"
f.write(f"## {status_icon} Security Scan Report (PR Files Only)\n\n")
if changed_files:
f.write("### Scanned Files\n")
for cf in changed_files[:10]: # Limit to first 10 files
f.write(f"- `{cf}`\n")
if len(changed_files) > 10:
f.write(f"- ... and {len(changed_files) - 10} more files\n")
f.write("\n---\n\n")
f.write("### Security Scan Results\n")
f.write("| Critical | High | Medium | Low | Info |\n")
f.write("|----------|------|--------|-----|---------|\n")
f.write(f"| {sev['critical']} | {sev['high']} | {sev['medium']} | {sev['low']} | {sev['info']} |\n\n")
f.write(f"**Threshold:** {threshold.title()}\n\n")
# Show critical and high issues
if findings:
f.write("---\n\n### Security Findings\n\n")
f.write("| Severity | Location | Description |\n")
f.write("|----------|----------|--------------|\n")
for finding in findings:
severity = finding['severity'].title()
file_line = f"{finding['file']}:{finding['line']}"
message = finding['message'].replace('|', '\\|').replace('\n', ' ') # Escape pipes and newlines for table
f.write(f"| {severity} | {file_line} | {message} |\n")
f.write("\n")
if not findings and (sev['critical'] > 0 or sev['high'] > 0):
f.write("Issues detected but detailed information not available. Check workflow artifacts.\n\n")
if sev['critical'] == 0 and sev['high'] == 0 and sev['medium'] == 0 and sev['low'] == 0:
f.write("No security issues detected in your changes. Great job!\n\n")
f.write("*This scan only covers files changed in this PR.*\n")
with open(os.environ["GITHUB_OUTPUT"], "a") as g:
g.write(f"fail={'true' if fail else 'false'}\n")
PY
- name: Save PR metadata for comment workflow
if: always()
run: |
echo "${{ github.event.pull_request.number }}" > pr_number.txt
echo "${{ github.event.pull_request.head.sha }}" > pr_sha.txt
if [ -f ash-summary.md ]; then
cp ash-summary.md pr_comment.md
else
echo "## ⚠️ ASH Security Scan Incomplete" > pr_comment.md
echo "Check workflow logs for details." >> pr_comment.md
fi
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: ash-security-results
path: |
pr-scan-dir/.ash/ash_output/reports/**
pr-scan-dir/.ash/ash_output/scanners/**
pr-scan-dir/.ash/ash_output/*.json
ash-output.log
ash-summary.md
pr_number.txt
pr_sha.txt
pr_comment.md
retention-days: 21
- name: Job summary
if: always()
run: |
echo "## ASH Scan Results" >> $GITHUB_STEP_SUMMARY
[ -f ash-summary.md ] && cat ash-summary.md >> $GITHUB_STEP_SUMMARY || echo "_No summary generated_" >> $GITHUB_STEP_SUMMARY
- name: Enforce severity threshold
if: steps.summarize.outputs.fail == 'true'
run: |
echo "Findings at/above ${FAIL_ON_SEVERITY}. Failing PR."
exit 1