Security Guard #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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') | |
| # Map severity to priority | |
| case "$VULN_SEVERITY" in | |
| critical) PRIORITY="Highest" ;; | |
| high) PRIORITY="High" ;; | |
| medium) PRIORITY="Medium" ;; | |
| *) PRIORITY="Low" ;; | |
| esac | |
| # 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 | |
| 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.\\n\\nType: $VULN_TYPE\\nFile: $VULN_FILE\\nSeverity: $VULN_SEVERITY\\nTotal vulnerabilities: $TOTAL\\n\\nDescription: $VULN_DESC\\n\\nPull Request: $PR_URL\" | |
| }] | |
| }] | |
| }, | |
| \"issuetype\": {\"name\": \"Bug\"}, | |
| \"priority\": {\"name\": \"$PRIORITY\"}, | |
| \"labels\": [\"security\", \"automated\", \"unitoneflow\"] | |
| } | |
| }") | |
| 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 // "unknown"') | |
| JIRA_TICKET_URL="$JIRA_URL/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 "❌ Failed to create Jira ticket" | |
| 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 | |
| echo "### Scan Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Vulnerabilities Found:** ${{ steps.detect.outputs.count || '0' }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| 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 | |
| if [ "${{ steps.create_jira.outputs.jira_url }}" != "" ]; then | |
| echo "### 🎫 Jira Ticket" >> $GITHUB_STEP_SUMMARY | |
| echo "**${{ steps.create_jira.outputs.jira_url }}**" >> $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 [ "${{ steps.create_jira.outputs.jira_url }}" != "" ]; then | |
| echo "🎫 Jira URL: ${{ steps.create_jira.outputs.jira_url }}" | |
| fi | |
| echo "" | |
| echo "==============================================" |