Skip to content

Security Guard

Security Guard #14

name: Security Guard
on:
workflow_dispatch:
inputs:
target_branch:
description: 'Branch to scan for vulnerabilities'
required: true
default: 'security-demo-clean'
type: string
jobs:
security-scan-and-fix:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.target_branch }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install anthropic requests pyyaml
- name: Install UnitOneFlow
run: |
pip install https://devflow-scanned-repos-dev.s3.us-west-2.amazonaws.com/public/wheels/unitoneflow-1.0.0-py3-none-any.whl
- name: Generate manifest
run: |
echo "🔍 Scanning codebase..."
cd backend
python -c "
import ast
import json
from pathlib import Path
def extract_functions(file_path):
functions = []
try:
content = file_path.read_text()
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
functions.append({
'name': node.name,
'file_path': str(file_path),
'line_number': node.lineno,
'end_line': node.end_lineno or node.lineno,
'docstring': ast.get_docstring(node) or ''
})
except:
pass
return functions
all_functions = []
for py_file in Path('.').rglob('*.py'):
if any(skip in str(py_file) for skip in ['venv', '__pycache__', 'test']):
continue
for func in extract_functions(py_file):
func['file_path'] = str(py_file)
all_functions.append(func)
manifest = {'functions': all_functions, 'files_scanned': len(list(Path('.').rglob('*.py')))}
Path('manifest.json').write_text(json.dumps(manifest, indent=2))
print(f'Created manifest with {len(all_functions)} functions')
"
- name: Detect vulnerabilities
id: detect
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
cd backend
python -c "
import anthropic
import json
from pathlib import Path
client = anthropic.Anthropic()
manifest = json.loads(Path('manifest.json').read_text())
# Build code context
code_context = []
seen_files = set()
for func in manifest.get('functions', [])[:20]:
fp = func.get('file_path', '')
if fp in seen_files:
continue
seen_files.add(fp)
p = Path(fp)
if p.exists():
code_context.append(f'### {fp}\n\`\`\`python\n{p.read_text()[:8000]}\n\`\`\`')
prompt = '''Analyze this Python codebase for security vulnerabilities.
Focus on: SQL Injection, Command Injection, Path Traversal, Code Injection, Insecure Deserialization.
Code Files:
''' + chr(10).join(code_context) + '''
For EACH vulnerability found, respond with a JSON array:
[{\"vulnerability_type\": \"...\", \"severity\": \"critical|high|medium|low\", \"file_path\": \"...\", \"line_number\": 0, \"description\": \"...\", \"vulnerable_code\": \"...\", \"recommendation\": \"...\"}]
If none found: []
Only output valid JSON.'''
response = client.messages.create(model='claude-sonnet-4-5-20250929', max_tokens=4096, messages=[{'role': 'user', 'content': prompt}])
text = response.content[0].text.strip()
try:
if text.startswith('['):
vulns = json.loads(text)
else:
start, end = text.find('['), text.rfind(']') + 1
vulns = json.loads(text[start:end]) if start >= 0 else []
except:
vulns = []
Path('security-report.json').write_text(json.dumps({'vulnerabilities': vulns, 'total': len(vulns)}, indent=2))
print(f'Found {len(vulns)} vulnerabilities')
# Set output
with open('$GITHUB_OUTPUT', 'a') as f:
f.write(f'count={len(vulns)}\n')
f.write(f'found={\"true\" if vulns else \"false\"}\n')
" 2>&1 || echo "Detection completed"
if [ -f security-report.json ]; then
VULN_COUNT=$(cat security-report.json | jq '.total // 0')
echo "count=$VULN_COUNT" >> $GITHUB_OUTPUT
if [ "$VULN_COUNT" -gt 0 ]; then
echo "found=true" >> $GITHUB_OUTPUT
else
echo "found=false" >> $GITHUB_OUTPUT
fi
else
echo "count=0" >> $GITHUB_OUTPUT
echo "found=false" >> $GITHUB_OUTPUT
fi
- name: Generate and apply fixes
if: steps.detect.outputs.found == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
cd backend
python -c "
import anthropic
import json
from pathlib import Path
client = anthropic.Anthropic()
report = json.loads(Path('security-report.json').read_text())
vulns = report.get('vulnerabilities', [])
for vuln in vulns[:3]: # Fix top 3 vulnerabilities
fp = vuln.get('file_path')
if not fp:
continue
p = Path(fp)
if not p.exists():
continue
original = p.read_text()
prompt = f'''Fix this security vulnerability:
Type: {vuln.get('vulnerability_type')}
File: {fp}
Line: {vuln.get('line_number')}
Issue: {vuln.get('description')}
Code: {vuln.get('vulnerable_code')}
Original file:
\`\`\`python
{original}
\`\`\`
Provide the COMPLETE fixed file. Only output code, no explanations.'''
response = client.messages.create(model='claude-sonnet-4-5-20250929', max_tokens=8192, messages=[{'role': 'user', 'content': prompt}])
fixed = response.content[0].text.strip()
if '\`\`\`' in fixed:
lines = fixed.split('\n')
in_code = False
code_lines = []
for line in lines:
if line.startswith('\`\`\`') and not in_code:
in_code = True
continue
elif line.startswith('\`\`\`') and in_code:
break
elif in_code:
code_lines.append(line)
if code_lines:
fixed = '\n'.join(code_lines)
# Validate fix doesn't truncate file significantly
if len(fixed) > len(original) * 0.5:
p.write_text(fixed)
print(f'Fixed: {fp}')
else:
print(f'Skipped {fp} - fix too short')
"
- name: Create PR with fixes
id: create_pr
if: steps.detect.outputs.found == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Work from repo root
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Check for Python file changes
if ! git diff --name-only | grep -q '\.py$'; then
echo "No Python file changes to commit"
echo "pr_created=false" >> $GITHUB_OUTPUT
exit 0
fi
# Create branch
BRANCH_NAME="fix/security-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH_NAME"
# Commit ONLY the Python files that were fixed
git diff --name-only | grep '\.py$' | xargs git add
git commit -m "fix: Security vulnerability fixes
Automated fixes by UnitOneFlow Security Guard.
Vulnerabilities addressed: ${{ steps.detect.outputs.count }}
See security-report.json for details."
# Push branch
git push -u origin "$BRANCH_NAME"
# Build PR description from security report
cd backend
VULN_SUMMARY=$(python3 -c "
import json
from pathlib import Path
report = json.loads(Path('security-report.json').read_text())
vulns = report.get('vulnerabilities', [])
# Count by severity
by_sev = {}
for v in vulns:
s = v.get('severity', 'unknown')
by_sev[s] = by_sev.get(s, 0) + 1
# Get top vulnerability for title
if vulns:
top = vulns[0]
print(f\"TOP_VULN={top.get('vulnerability_type', 'Security Issue')}\")
print(f\"TOP_FILE={top.get('file_path', 'unknown')}\")
# Build severity summary
sev_order = ['critical', 'high', 'medium', 'low']
parts = []
for s in sev_order:
if s in by_sev:
parts.append(f'{by_sev[s]} {s}')
print(f\"SEV_SUMMARY={', '.join(parts)}\")
# Build details table
print('DETAILS_START')
print('| Severity | Type | File | Line |')
print('|----------|------|------|------|')
for v in vulns[:10]:
sev = v.get('severity', 'unknown').upper()
vtype = v.get('vulnerability_type', 'Unknown')
fp = v.get('file_path', 'unknown')
line = v.get('line_number', '?')
print(f'| {sev} | {vtype} | \`{fp}\` | {line} |')
print('DETAILS_END')
")
cd ..
TOP_VULN=$(echo "$VULN_SUMMARY" | grep "TOP_VULN=" | cut -d= -f2)
TOP_FILE=$(echo "$VULN_SUMMARY" | grep "TOP_FILE=" | cut -d= -f2)
SEV_SUMMARY=$(echo "$VULN_SUMMARY" | grep "SEV_SUMMARY=" | cut -d= -f2)
DETAILS_TABLE=$(echo "$VULN_SUMMARY" | sed -n '/DETAILS_START/,/DETAILS_END/p' | grep -v "DETAILS_")
PR_TITLE="[Security] Fix $TOP_VULN in $TOP_FILE (+${{ steps.detect.outputs.count }} vulnerabilities)"
# Write PR body to file
cat > /tmp/pr_body.md << PRBODYEOF
## Security Vulnerability Fixes
Automated by UnitOneFlow Security Guard
### Summary
- Total vulnerabilities fixed: ${{ steps.detect.outputs.count }}
- Severity breakdown: $SEV_SUMMARY
### Vulnerabilities Addressed
$DETAILS_TABLE
### Changes Made
- Added input validation and sanitization
- Fixed insecure code patterns
- See diff for details
---
Generated by UnitOneFlow Security Guard
PRBODYEOF
# Create PR
PR_URL=$(gh pr create --title "$PR_TITLE" --body-file /tmp/pr_body.md --base "${{ inputs.target_branch }}")
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
echo "pr_created=true" >> $GITHUB_OUTPUT
echo "PR created: $PR_URL"
- name: Create Jira ticket
id: create_jira
if: steps.detect.outputs.found == 'true'
env:
JIRA_URL: ${{ secrets.JIRA_URL }}
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }}
run: |
cd backend
if [ -z "$JIRA_URL" ]; then
echo "Jira not configured, skipping"
exit 0
fi
# Get vulnerability details
VULN_TYPE=$(cat security-report.json | jq -r '.vulnerabilities[0].vulnerability_type // "Security Issue"')
VULN_FILE=$(cat security-report.json | jq -r '.vulnerabilities[0].file_path // "unknown"')
VULN_DESC=$(cat security-report.json | jq -r '.vulnerabilities[0].description // "Security vulnerability detected"')
VULN_SEVERITY=$(cat security-report.json | jq -r '.vulnerabilities[0].severity // "medium"')
TOTAL=$(cat security-report.json | jq '.total // 0')
# Get PR URL from previous step
PR_URL="${{ steps.create_pr.outputs.pr_url }}"
if [ -z "$PR_URL" ]; then
PR_URL="PR creation pending"
fi
echo "Creating Jira ticket..."
echo "PR URL: $PR_URL"
# Create Jira ticket (simplified - no priority/labels to avoid field errors)
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$JIRA_URL/rest/api/3/issue" \
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"fields\": {
\"project\": {\"key\": \"$JIRA_PROJECT\"},
\"summary\": \"[Security] $VULN_TYPE in $VULN_FILE\",
\"description\": {
\"type\": \"doc\",
\"version\": 1,
\"content\": [{
\"type\": \"paragraph\",
\"content\": [{
\"type\": \"text\",
\"text\": \"Security vulnerability detected by UnitOneFlow Security Guard.\"
}]
}, {
\"type\": \"paragraph\",
\"content\": [{
\"type\": \"text\",
\"text\": \"Type: $VULN_TYPE\"
}]
}, {
\"type\": \"paragraph\",
\"content\": [{
\"type\": \"text\",
\"text\": \"File: $VULN_FILE\"
}]
}, {
\"type\": \"paragraph\",
\"content\": [{
\"type\": \"text\",
\"text\": \"Severity: $VULN_SEVERITY\"
}]
}, {
\"type\": \"paragraph\",
\"content\": [{
\"type\": \"text\",
\"text\": \"Total vulnerabilities: $TOTAL\"
}]
}, {
\"type\": \"paragraph\",
\"content\": [{
\"type\": \"text\",
\"text\": \"Pull Request: $PR_URL\"
}]
}]
},
\"issuetype\": {\"name\": \"Task\"}
}
}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "Jira API response code: $HTTP_CODE"
echo "Jira API response: $BODY"
if [ "$HTTP_CODE" = "201" ]; then
TICKET_KEY=$(echo "$BODY" | jq -r '.key // ""')
if [ -n "$TICKET_KEY" ] && [ "$TICKET_KEY" != "null" ]; then
# Construct full Jira URL - ensure base URL is valid
JIRA_BASE="${JIRA_URL%/}" # Remove trailing slash if present
if [ -z "$JIRA_BASE" ]; then
JIRA_BASE="https://unitoneai.atlassian.net"
fi
JIRA_TICKET_URL="${JIRA_BASE}/browse/${TICKET_KEY}"
echo "jira_url=$JIRA_TICKET_URL" >> $GITHUB_OUTPUT
echo "jira_key=$TICKET_KEY" >> $GITHUB_OUTPUT
echo "✅ Jira ticket created: $JIRA_TICKET_URL"
else
echo "❌ Jira ticket created but could not parse key"
echo "jira_url=" >> $GITHUB_OUTPUT
fi
else
echo "❌ Failed to create Jira ticket (HTTP $HTTP_CODE)"
echo "Response: $BODY"
echo "jira_url=" >> $GITHUB_OUTPUT
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
backend/manifest.json
backend/security-report.json
- name: Job Summary
if: always()
run: |
echo "## 🔒 Security Guard Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Links section
if [ "${{ steps.create_pr.outputs.pr_url }}" != "" ]; then
echo "### 📝 Pull Request" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.create_pr.outputs.pr_url }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
JIRA_URL="${{ steps.create_jira.outputs.jira_url }}"
if [ -n "$JIRA_URL" ] && [[ "$JIRA_URL" == http* ]]; then
echo "### 🎫 Jira Ticket" >> $GITHUB_STEP_SUMMARY
echo "$JIRA_URL" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "### 📊 Scan Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Vulnerabilities Found:** ${{ steps.detect.outputs.count || '0' }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Vulnerability table
if [ -f backend/security-report.json ]; then
echo "### 🔍 Vulnerabilities Detected" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Severity | Type | File | Line |" >> $GITHUB_STEP_SUMMARY
echo "|----------|------|------|------|" >> $GITHUB_STEP_SUMMARY
python3 -c "
import json
report = json.loads(open('backend/security-report.json').read())
for v in report.get('vulnerabilities', [])[:15]:
sev = v.get('severity', 'unknown').upper()
vtype = v.get('vulnerability_type', 'Unknown')
fp = v.get('file_path', 'unknown')
line = v.get('line_number', '?')
print(f'| {sev} | {vtype} | \`{fp}\` | {line} |')
" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "---" >> $GITHUB_STEP_SUMMARY
echo "*Generated by [UnitOneFlow Security Guard](https://github.com/UnitOneAI/unitoneflow)*" >> $GITHUB_STEP_SUMMARY
echo ""
echo "=============================================="
echo "🔒 SECURITY GUARD COMPLETE"
echo "=============================================="
echo ""
echo "📊 Vulnerabilities Found: ${{ steps.detect.outputs.count || '0' }}"
echo ""
if [ "${{ steps.create_pr.outputs.pr_url }}" != "" ]; then
echo "📝 PR URL: ${{ steps.create_pr.outputs.pr_url }}"
fi
if [ -n "$JIRA_URL" ] && [[ "$JIRA_URL" == http* ]]; then
echo "🎫 Jira URL: $JIRA_URL"
fi
echo ""
echo "=============================================="