Skip to content

Commit 4296014

Browse files
feat: Add fully automated Security Guard workflow
Automated security vulnerability detection and fixing: - Triggers on push (no manual intervention) - Scans code and detects vulnerabilities using Claude - Generates and applies fixes automatically - Creates PR with fixes - Creates Jira ticket with PR link No manual steps required - fully automated pipeline. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9762ef3 commit 4296014

1 file changed

Lines changed: 334 additions & 0 deletions

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
name: Security Guard
2+
3+
on:
4+
push:
5+
branches: [main, security-demo-clean]
6+
workflow_dispatch:
7+
8+
jobs:
9+
security-scan-and-fix:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
pull-requests: write
14+
issues: write
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
token: ${{ secrets.GITHUB_TOKEN }}
22+
23+
- name: Setup Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: '3.11'
27+
28+
- name: Install dependencies
29+
run: |
30+
pip install anthropic requests pyyaml
31+
32+
- name: Install UnitOneFlow
33+
run: |
34+
pip install https://devflow-scanned-repos-dev.s3.us-west-2.amazonaws.com/public/wheels/unitoneflow-1.0.0-py3-none-any.whl
35+
36+
- name: Generate manifest
37+
run: |
38+
echo "🔍 Scanning codebase..."
39+
cd backend
40+
python -c "
41+
import ast
42+
import json
43+
from pathlib import Path
44+
45+
def extract_functions(file_path):
46+
functions = []
47+
try:
48+
content = file_path.read_text()
49+
tree = ast.parse(content)
50+
for node in ast.walk(tree):
51+
if isinstance(node, ast.FunctionDef):
52+
functions.append({
53+
'name': node.name,
54+
'file_path': str(file_path),
55+
'line_number': node.lineno,
56+
'end_line': node.end_lineno or node.lineno,
57+
'docstring': ast.get_docstring(node) or ''
58+
})
59+
except:
60+
pass
61+
return functions
62+
63+
all_functions = []
64+
for py_file in Path('.').rglob('*.py'):
65+
if any(skip in str(py_file) for skip in ['venv', '__pycache__', 'test']):
66+
continue
67+
for func in extract_functions(py_file):
68+
func['file_path'] = str(py_file)
69+
all_functions.append(func)
70+
71+
manifest = {'functions': all_functions, 'files_scanned': len(list(Path('.').rglob('*.py')))}
72+
Path('manifest.json').write_text(json.dumps(manifest, indent=2))
73+
print(f'Created manifest with {len(all_functions)} functions')
74+
"
75+
76+
- name: Detect vulnerabilities
77+
id: detect
78+
env:
79+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
80+
run: |
81+
cd backend
82+
python -c "
83+
import anthropic
84+
import json
85+
from pathlib import Path
86+
87+
client = anthropic.Anthropic()
88+
manifest = json.loads(Path('manifest.json').read_text())
89+
90+
# Build code context
91+
code_context = []
92+
seen_files = set()
93+
for func in manifest.get('functions', [])[:20]:
94+
fp = func.get('file_path', '')
95+
if fp in seen_files:
96+
continue
97+
seen_files.add(fp)
98+
p = Path(fp)
99+
if p.exists():
100+
code_context.append(f'### {fp}\n\`\`\`python\n{p.read_text()[:8000]}\n\`\`\`')
101+
102+
prompt = '''Analyze this Python codebase for security vulnerabilities.
103+
Focus on: SQL Injection, Command Injection, Path Traversal, Code Injection, Insecure Deserialization.
104+
105+
Code Files:
106+
''' + chr(10).join(code_context) + '''
107+
108+
For EACH vulnerability found, respond with a JSON array:
109+
[{\"vulnerability_type\": \"...\", \"severity\": \"critical|high|medium|low\", \"file_path\": \"...\", \"line_number\": 0, \"description\": \"...\", \"vulnerable_code\": \"...\", \"recommendation\": \"...\"}]
110+
If none found: []
111+
Only output valid JSON.'''
112+
113+
response = client.messages.create(model='claude-sonnet-4-5-20250929', max_tokens=4096, messages=[{'role': 'user', 'content': prompt}])
114+
text = response.content[0].text.strip()
115+
116+
try:
117+
if text.startswith('['):
118+
vulns = json.loads(text)
119+
else:
120+
start, end = text.find('['), text.rfind(']') + 1
121+
vulns = json.loads(text[start:end]) if start >= 0 else []
122+
except:
123+
vulns = []
124+
125+
Path('security-report.json').write_text(json.dumps({'vulnerabilities': vulns, 'total': len(vulns)}, indent=2))
126+
print(f'Found {len(vulns)} vulnerabilities')
127+
128+
# Set output
129+
with open('$GITHUB_OUTPUT', 'a') as f:
130+
f.write(f'count={len(vulns)}\n')
131+
f.write(f'found={\"true\" if vulns else \"false\"}\n')
132+
" 2>&1 || echo "Detection completed"
133+
134+
if [ -f security-report.json ]; then
135+
VULN_COUNT=$(cat security-report.json | jq '.total // 0')
136+
echo "count=$VULN_COUNT" >> $GITHUB_OUTPUT
137+
if [ "$VULN_COUNT" -gt 0 ]; then
138+
echo "found=true" >> $GITHUB_OUTPUT
139+
else
140+
echo "found=false" >> $GITHUB_OUTPUT
141+
fi
142+
else
143+
echo "count=0" >> $GITHUB_OUTPUT
144+
echo "found=false" >> $GITHUB_OUTPUT
145+
fi
146+
147+
- name: Generate and apply fixes
148+
if: steps.detect.outputs.found == 'true'
149+
env:
150+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
151+
run: |
152+
cd backend
153+
python -c "
154+
import anthropic
155+
import json
156+
from pathlib import Path
157+
158+
client = anthropic.Anthropic()
159+
report = json.loads(Path('security-report.json').read_text())
160+
vulns = report.get('vulnerabilities', [])
161+
162+
for vuln in vulns[:3]: # Fix top 3 vulnerabilities
163+
fp = vuln.get('file_path')
164+
if not fp:
165+
continue
166+
p = Path(fp)
167+
if not p.exists():
168+
continue
169+
170+
original = p.read_text()
171+
prompt = f'''Fix this security vulnerability:
172+
Type: {vuln.get('vulnerability_type')}
173+
File: {fp}
174+
Line: {vuln.get('line_number')}
175+
Issue: {vuln.get('description')}
176+
Code: {vuln.get('vulnerable_code')}
177+
178+
Original file:
179+
\`\`\`python
180+
{original}
181+
\`\`\`
182+
183+
Provide the COMPLETE fixed file. Only output code, no explanations.'''
184+
185+
response = client.messages.create(model='claude-sonnet-4-5-20250929', max_tokens=8192, messages=[{'role': 'user', 'content': prompt}])
186+
fixed = response.content[0].text.strip()
187+
188+
if '\`\`\`' in fixed:
189+
lines = fixed.split('\n')
190+
in_code = False
191+
code_lines = []
192+
for line in lines:
193+
if line.startswith('\`\`\`') and not in_code:
194+
in_code = True
195+
continue
196+
elif line.startswith('\`\`\`') and in_code:
197+
break
198+
elif in_code:
199+
code_lines.append(line)
200+
if code_lines:
201+
fixed = '\n'.join(code_lines)
202+
203+
# Validate fix doesn't truncate file significantly
204+
if len(fixed) > len(original) * 0.5:
205+
p.write_text(fixed)
206+
print(f'Fixed: {fp}')
207+
else:
208+
print(f'Skipped {fp} - fix too short')
209+
"
210+
211+
- name: Create PR with fixes
212+
if: steps.detect.outputs.found == 'true'
213+
env:
214+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
215+
run: |
216+
cd backend
217+
218+
# Check for changes
219+
git config user.name "github-actions[bot]"
220+
git config user.email "github-actions[bot]@users.noreply.github.com"
221+
222+
if git diff --quiet; then
223+
echo "No changes to commit"
224+
echo "pr_created=false" >> $GITHUB_OUTPUT
225+
exit 0
226+
fi
227+
228+
# Create branch
229+
BRANCH_NAME="fix/security-$(date +%Y%m%d-%H%M%S)"
230+
git checkout -b "$BRANCH_NAME"
231+
232+
# Commit changes
233+
git add -A
234+
git commit -m "fix: Security vulnerability fixes
235+
236+
Automated fixes by UnitOneFlow Security Guard.
237+
238+
Vulnerabilities addressed: ${{ steps.detect.outputs.count }}
239+
240+
See security-report.json for details."
241+
242+
# Push branch
243+
git push -u origin "$BRANCH_NAME"
244+
245+
# Create PR
246+
PR_URL=$(gh pr create \
247+
--title "[Security] Fix ${{ steps.detect.outputs.count }} vulnerability(s)" \
248+
--body "## Security Vulnerability Fixes
249+
250+
**Automated by UnitOneFlow Security Guard**
251+
252+
### Summary
253+
- Vulnerabilities detected: ${{ steps.detect.outputs.count }}
254+
- Fixes applied: See diff
255+
256+
### Details
257+
See \`security-report.json\` in artifacts.
258+
259+
---
260+
*Generated by [UnitOneFlow](https://github.com/UnitOneAI/unitoneflow)*" \
261+
--base "${{ github.ref_name }}")
262+
263+
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
264+
echo "pr_created=true" >> $GITHUB_OUTPUT
265+
echo "PR created: $PR_URL"
266+
267+
- name: Create Jira ticket
268+
if: steps.detect.outputs.found == 'true'
269+
env:
270+
JIRA_URL: ${{ secrets.JIRA_URL }}
271+
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
272+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
273+
JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }}
274+
run: |
275+
cd backend
276+
277+
if [ -z "$JIRA_URL" ]; then
278+
echo "Jira not configured, skipping"
279+
exit 0
280+
fi
281+
282+
# Get vulnerability details
283+
VULN_TYPE=$(cat security-report.json | jq -r '.vulnerabilities[0].vulnerability_type // "Security Issue"')
284+
VULN_FILE=$(cat security-report.json | jq -r '.vulnerabilities[0].file_path // "unknown"')
285+
VULN_DESC=$(cat security-report.json | jq -r '.vulnerabilities[0].description // "Security vulnerability detected"')
286+
VULN_SEVERITY=$(cat security-report.json | jq -r '.vulnerabilities[0].severity // "medium"')
287+
TOTAL=$(cat security-report.json | jq '.total // 0')
288+
289+
# Map severity to priority
290+
case "$VULN_SEVERITY" in
291+
critical) PRIORITY="Highest" ;;
292+
high) PRIORITY="High" ;;
293+
medium) PRIORITY="Medium" ;;
294+
*) PRIORITY="Low" ;;
295+
esac
296+
297+
# Get PR URL if available
298+
PR_URL="${{ steps.create_pr.outputs.pr_url || 'PR pending' }}"
299+
300+
# Create Jira ticket
301+
RESPONSE=$(curl -s -X POST "$JIRA_URL/rest/api/3/issue" \
302+
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
303+
-H "Content-Type: application/json" \
304+
-d "{
305+
\"fields\": {
306+
\"project\": {\"key\": \"$JIRA_PROJECT\"},
307+
\"summary\": \"[Security] $VULN_TYPE in $VULN_FILE\",
308+
\"description\": {
309+
\"type\": \"doc\",
310+
\"version\": 1,
311+
\"content\": [{
312+
\"type\": \"paragraph\",
313+
\"content\": [{
314+
\"type\": \"text\",
315+
\"text\": \"Security vulnerability detected by UnitOneFlow Security Guard.\\n\\nType: $VULN_TYPE\\nFile: $VULN_FILE\\nSeverity: $VULN_SEVERITY\\nTotal vulnerabilities: $TOTAL\\n\\nDescription: $VULN_DESC\\n\\nPull Request: $PR_URL\"
316+
}]
317+
}]
318+
},
319+
\"issuetype\": {\"name\": \"Bug\"},
320+
\"priority\": {\"name\": \"$PRIORITY\"},
321+
\"labels\": [\"security\", \"automated\", \"unitoneflow\"]
322+
}
323+
}")
324+
325+
TICKET_KEY=$(echo "$RESPONSE" | jq -r '.key // "unknown"')
326+
echo "Jira ticket created: $JIRA_URL/browse/$TICKET_KEY"
327+
328+
- name: Upload artifacts
329+
uses: actions/upload-artifact@v4
330+
with:
331+
name: security-reports
332+
path: |
333+
backend/manifest.json
334+
backend/security-report.json

0 commit comments

Comments
 (0)