diff --git a/.ash.yaml b/.ash.yaml new file mode 100644 index 000000000..11e340189 --- /dev/null +++ b/.ash.yaml @@ -0,0 +1,69 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/awslabs/automated-security-helper/refs/heads/main/automated_security_helper/schemas/AshConfig.json + +project_name: amazon-bedrock-agentcore-samples + +global_settings: + severity_threshold: MEDIUM + fail_on_findings: false # Don't fail CI, just report + ignore_paths: [] + +scanners: + bandit: + enabled: true + options: + confidence_level: MEDIUM + severity_level: medium + + semgrep: + enabled: true + options: + rules: ['p/ci', 'p/security-audit'] + timeout: 300 + + detect-secrets: + enabled: true + options: + exclude_lines: [] + + checkov: + enabled: true + options: + framework: ['all'] + + cfn-nag: + enabled: true + + cdk-nag: + enabled: true + options: + nag_packs: ['AWS_SOLUTIONS'] + + npm-audit: + enabled: true + options: + audit_level: moderate + + grype: + enabled: true + options: + severity: medium + + syft: + enabled: true + +reporters: + markdown: + enabled: true + options: + include_detailed_findings: true + max_detailed_findings: 1000 # Show all findings instead of default 20 + + json: + enabled: true + options: + pretty_print: true + + sarif: + enabled: true + options: + include_help_uri: true \ No newline at end of file diff --git a/.github/workflows/ash-full-repository-scan.yml b/.github/workflows/ash-full-repository-scan.yml new file mode 100644 index 000000000..259cf2c77 --- /dev/null +++ b/.github/workflows/ash-full-repository-scan.yml @@ -0,0 +1,170 @@ +name: ASH Full Repository Scan + +on: + push: + branches: [ main ] + schedule: + # Run at 2 AM UTC on the 1st of every month + - cron: '0 2 1 * *' + workflow_dispatch: # Allow manual triggering + +permissions: + contents: read + security-events: write # For uploading SARIF results to GitHub Security tab + +jobs: + full-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install ASH + run: pip install git+https://github.com/awslabs/automated-security-helper.git@v3.0.0 + + - name: Run ASH full repository scan + run: | + # Create ASH config for comprehensive scanning + cat > .ash_config.yaml << 'EOF' + reporters: + markdown: + enabled: true + options: + include_detailed_findings: true + max_detailed_findings: 1000 + sarif: + enabled: true + EOF + + # Run ASH on entire repository + ash --mode container --config .ash_config.yaml 2>&1 | tee ash-output.log + continue-on-error: true + + - name: Generate scan summary + id: scan-summary + run: | + SUMMARY_FILE="ash-summary.md" + + echo "# ASH Security Scan - Full Repository Report" > "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Scan Date:** $(date -u +%Y-%m-%dT%H:%M:%S+00:00)" >> "$SUMMARY_FILE" + echo "**Trigger:** ${{ github.event_name }}" >> "$SUMMARY_FILE" + if [ "${{ github.event_name }}" == "push" ]; then + echo "**Commit:** ${{ github.sha }}" >> "$SUMMARY_FILE" + echo "**Pushed by:** ${{ github.actor }}" >> "$SUMMARY_FILE" + elif [ "${{ github.event_name }}" == "schedule" ]; then + echo "**Type:** Monthly scheduled scan" >> "$SUMMARY_FILE" + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "**Type:** Manual trigger by ${{ github.actor }}" >> "$SUMMARY_FILE" + fi + echo "" >> "$SUMMARY_FILE" + + # Extract and format scan results + if [ -f "ash-output.log" ]; then + # Find the table boundaries + TABLE_START=$(grep -n "ASH Scan Results Summary" ash-output.log | head -1 | cut -d: -f1 || echo "0") + TABLE_END=$(grep -n "source-dir:" ash-output.log | head -1 | cut -d: -f1 || echo "0") + + if [ "$TABLE_START" != "0" ] && [ "$TABLE_END" != "0" ] && [ "$TABLE_END" -gt "$TABLE_START" ]; then + echo "## Scanner Results Summary" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + + # Convert terminal table to markdown + echo "| Scanner | S | C | H | M | L | I | Time | Action | Result | Thresh |" >> "$SUMMARY_FILE" + echo "|---------|---|---|---|---|---|---|------|--------|--------|--------|" >> "$SUMMARY_FILE" + sed -n "${TABLE_START},${TABLE_END}p" ash-output.log | \ + sed 's/\x1b\[[0-9;]*m//g' | \ + grep "^│" | \ + sed 's/│/|/g' | \ + sed 's/^ *|/|/' | \ + sed 's/| *$/|/' >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + fi + + # Check for findings + if grep -q "Actionable findings detected!" ash-output.log; then + echo "has_findings=true" >> $GITHUB_OUTPUT + echo "**Status:** ⚠️ Security findings detected" >> "$SUMMARY_FILE" + else + echo "has_findings=false" >> $GITHUB_OUTPUT + echo "**Status:** ✅ No security issues found" >> "$SUMMARY_FILE" + fi + fi + + # Include detailed findings if available + if [ -f ".ash/ash_output/reports/ash.summary.md" ]; then + echo "" >> "$SUMMARY_FILE" + echo "## Detailed Findings" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + grep -A 1000 "Detailed Findings" ".ash/ash_output/reports/ash.summary.md" | \ + grep -v -E '^(Time since scan:|Report generated:)' | \ + grep -v 'Report generated by Automated Security Helper' >> "$SUMMARY_FILE" || true + fi + + - name: Upload ASH results as artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: ash-full-scan-${{ github.run_id }} + path: | + .ash/ + ash-output.log + ash-summary.md + retention-days: 90 + + - name: Upload SARIF results to GitHub Security + if: always() && hashFiles('.ash/ash_output/reports/ash.sarif') != '' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: .ash/ash_output/reports/ash.sarif + category: ash-security-scan + + - name: Create issue for critical findings (monthly scan only) + if: github.event_name == 'schedule' && steps.scan-summary.outputs.has_findings == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const summaryPath = 'ash-summary.md'; + + if (fs.existsSync(summaryPath)) { + const summaryContent = fs.readFileSync(summaryPath, 'utf8'); + + // Create issue for monthly scan findings + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `🔒 ASH Security Scan - Monthly Report (${new Date().toISOString().split('T')[0]})`, + body: summaryContent + '\n\n---\n*This issue was automatically created by the monthly security scan workflow.*', + labels: ['security', 'automated-scan'] + }); + } + + - name: Job summary + if: always() + run: | + echo "## ASH Security Scan Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "ash-summary.md" ]; then + cat ash-summary.md >> $GITHUB_STEP_SUMMARY + else + echo "No scan summary available." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "*Full scan results are available in the [workflow artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.scan-summary.outputs.has_findings }}" == "true" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ **Action Required:** Security findings were detected. Please review the results and address any critical issues." >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.github/workflows/ash-security-comment.yml b/.github/workflows/ash-security-comment.yml new file mode 100644 index 000000000..873742918 --- /dev/null +++ b/.github/workflows/ash-security-comment.yml @@ -0,0 +1,131 @@ +name: ASH Security Scan - Post Comments + +on: + workflow_run: + workflows: ["ASH Security Scan"] + types: + - completed + +permissions: + pull-requests: write + actions: read + +jobs: + comment: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ash-security-results + path: /tmp/ash-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get PR information + id: pr-info + run: | + if [ -f /tmp/ash-results/pr_number.txt ]; then + PR_NUMBER=$(cat /tmp/ash-results/pr_number.txt) + echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT + echo "Found PR number: ${PR_NUMBER}" + else + echo "No PR number found in artifacts" + exit 1 + fi + + if [ -f /tmp/ash-results/pr_sha.txt ]; then + PR_SHA=$(cat /tmp/ash-results/pr_sha.txt) + echo "pr_sha=${PR_SHA}" >> $GITHUB_OUTPUT + echo "Found PR SHA: ${PR_SHA}" + fi + + - name: Post comment on PR + if: steps.pr-info.outputs.pr_number + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const commentPath = '/tmp/ash-results/pr_comment.md'; + + if (!fs.existsSync(commentPath)) { + console.log('No comment file found in artifacts'); + return; + } + + const commentBody = fs.readFileSync(commentPath, 'utf8'); + const prNumber = parseInt('${{ steps.pr-info.outputs.pr_number }}'); + const prSha = '${{ steps.pr-info.outputs.pr_sha }}'; + + if (!prNumber) { + console.log('Invalid PR number'); + return; + } + + // Get existing comments + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + // Find ALL ASH security scan comments + const ashComments = comments.filter(comment => + comment.user.type === 'Bot' && + (comment.body.includes('') || + comment.body.includes('## Security Scan Results') || + comment.body.includes('Latest scan for commit:') || + comment.body.includes('ASH Security Scan Report')) + ); + + console.log(`Found ${ashComments.length} ASH security scan comments`); + + // Use the most recent ASH comment (highest ID = most recent) + const existingComment = ashComments.length > 0 ? + ashComments.sort((a, b) => b.id - a.id)[0] : null; + + // Delete any duplicate/older ASH comments (keep only the most recent one) + if (ashComments.length > 1) { + console.log(`Cleaning up ${ashComments.length - 1} duplicate ASH comments`); + for (const comment of ashComments.slice(1)) { + try { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id, + }); + console.log(`Deleted duplicate comment ${comment.id}`); + } catch (error) { + console.log(`Failed to delete comment ${comment.id}: ${error.message}`); + } + } + } + + // Add commit and timestamp info to the body + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; + const shortSha = prSha ? prSha.substring(0, 7) : 'unknown'; + const enhancedBody = `**Latest scan for commit:** \`${shortSha}\` **| Updated:** ${timestamp}\n\n${commentBody}\n\n`; + + if (existingComment) { + // Update existing comment + console.log(`Updating existing comment ${existingComment.id}`); + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: enhancedBody + }); + console.log('Successfully updated existing ASH security scan comment'); + } else { + // Create new comment + console.log('No existing ASH comment found, creating new one'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: enhancedBody + }); + console.log('Successfully created new ASH security scan comment'); + } \ No newline at end of file diff --git a/.github/workflows/ash-security-scan.yml b/.github/workflows/ash-security-scan.yml index b81eb0e01..6ca18a218 100644 --- a/.github/workflows/ash-security-scan.yml +++ b/.github/workflows/ash-security-scan.yml @@ -1,4 +1,4 @@ -name: ASH Security Scan with PR Comments +name: ASH Security Scan on: pull_request: @@ -6,7 +6,6 @@ on: permissions: contents: read - pull-requests: write jobs: scan: @@ -198,7 +197,13 @@ jobs: fi - - name: Upload ASH results + - name: Save PR metadata + if: steps.changed-files.outputs.any_changed == 'true' + run: | + echo "${{ github.event.pull_request.number }}" > /tmp/pr_number.txt + echo "${{ github.event.pull_request.head.sha }}" > /tmp/pr_sha.txt + + - name: Upload ASH results and PR metadata if: steps.changed-files.outputs.any_changed == 'true' && always() uses: actions/upload-artifact@v4 with: @@ -206,100 +211,10 @@ jobs: path: | /tmp/ash-scan/.ash/ /tmp/pr_comment.md + /tmp/pr_number.txt + /tmp/pr_sha.txt retention-days: 30 - - name: Add PR comment - if: steps.changed-files.outputs.any_changed == 'true' - # Always run this step even if ASH scan failed - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const commentPath = '/tmp/pr_comment.md'; - - if (fs.existsSync(commentPath)) { - const commentBody = fs.readFileSync(commentPath, 'utf8'); - const issueNumber = context.issue.number; - - // Check if we already have an ASH security scan comment - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - }); - - // Debug: Log all bot comments to understand what we have - const botComments = comments.filter(comment => comment.user.type === 'Bot'); - console.log(`Found ${botComments.length} bot comments`); - botComments.forEach(comment => { - console.log(`Bot comment ${comment.id}: ${comment.body.substring(0, 100)}...`); - }); - - // Find ALL ASH security scan comments - const ashComments = comments.filter(comment => - comment.user.type === 'Bot' && - (comment.body.includes('') || - comment.body.includes('## Security Scan Results') || - comment.body.includes('Latest scan for commit:') || - comment.body.includes('ASH Security Scan Report')) - ); - - console.log(`Found ${ashComments.length} ASH security scan comments`); - - // Use the most recent ASH comment (highest ID = most recent) - const botComment = ashComments.length > 0 ? - ashComments.sort((a, b) => b.id - a.id)[0] : null; - - if (botComment) { - console.log(`Will update most recent ASH comment: ${botComment.id}`); - } - - // Delete any duplicate/older ASH comments (keep only the most recent one) - if (ashComments.length > 1) { - console.log(`Cleaning up ${ashComments.length - 1} duplicate ASH comments`); - for (const comment of ashComments.slice(1)) { // Skip the first (most recent) one - try { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: comment.id, - }); - console.log(`Deleted duplicate comment ${comment.id}`); - } catch (error) { - console.log(`Failed to delete comment ${comment.id}: ${error.message}`); - } - } - } - - // Add commit and timestamp info to the body with unique identifier - const commitSha = context.payload.pull_request?.head?.sha || context.sha; - const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; - const enhancedBody = `**Latest scan for commit:** \`${commitSha.substring(0, 7)}\` **| Updated:** ${timestamp}\n\n${commentBody}\n\n`; - - if (botComment) { - // Update existing comment with latest scan - console.log(`Updating existing comment ${botComment.id}`); - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: enhancedBody - }); - console.log('Successfully updated existing ASH security scan comment'); - } else { - // Create new comment - console.log('No existing ASH comment found, creating new one'); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: enhancedBody - }); - console.log('Successfully created new ASH security scan comment'); - } - } - - name: Security scan summary if: steps.changed-files.outputs.any_changed == 'true' run: | diff --git a/ash_test_files/README.md b/ash_test_files/README.md new file mode 100644 index 000000000..08b066f92 --- /dev/null +++ b/ash_test_files/README.md @@ -0,0 +1,25 @@ +# ASH Scanner Validation Test + +This directory contains renamed test files to validate that the ASH security scanner workflow runs all scanners properly without skipping them. + +## Test Files + +- **bad_python_code.py** - Python with linting and formatting issues +- **bad_javascript_code.js** - JavaScript with various code quality problems +- **security_vulnerabilities.py** - Python with intentional security vulnerabilities +- **security_vulnerabilities.js** - JavaScript with security issues + +## Expected Results + +With the updated ASH workflow that installs scanner dependencies, all scanners should now show PASSED/FAILED status instead of SKIPPED: + +- **bandit** - Should detect Python security issues +- **semgrep** - Should detect security patterns in both languages +- **detect-secrets** - Should find hardcoded secrets +- **checkov** - Should run infrastructure checks + +## Purpose + +This tests the fix for the ASH workflow where scanners were being skipped due to missing UV dependencies. The workflow now manually installs scanner tools via pip. + +**WARNING: These files contain intentional vulnerabilities for testing only!** \ No newline at end of file diff --git a/ash_test_files/bad_javascript_code.js b/ash_test_files/bad_javascript_code.js new file mode 100644 index 000000000..8da51fab2 --- /dev/null +++ b/ash_test_files/bad_javascript_code.js @@ -0,0 +1,143 @@ +// JavaScript file with various linting issues + +// Missing semicolons +var x = 5 +var y = 10 + +// Using var instead of let/const +var oldStyle = "should use const" + +// Inconsistent quotes +const mixed = "double" + 'single'; + +// Missing JSDoc comments +function undocumented(a, b) { + return a + b +} + +// Trailing spaces and inconsistent indentation +function badFormatting() { + var result = 0; + for(var i=0;i<10;i++) + { + result+=i; + } + return result +} + +// Using == instead of === +function loosEquality(val) { + if (val == null) { + return true + } + if (val != 0) { + return false; + } +} + +// Unused variables +function unusedVars() { + const unused1 = 'not used'; + let unused2 = 42; + var result = 100; + return result; +} + +// Missing error handling +function riskyOperation() { + const data = JSON.parse(userInput); // userInput not defined + return data.value; +} + +// Global variable pollution +globalVar = 'should not use globals'; + +// Callback hell / deeply nested +function callbackHell(callback) { + getData(function(a) { + getMoreData(a, function(b) { + getMoreData(b, function(c) { + getMoreData(c, function(d) { + callback(d); + }); + }); + }); + }); +} + +// Arrow function without parentheses for single param (style issue) +const arrow = x => x * 2 + +// Missing const/let/var declaration +implicitGlobal = 100; + +// Redeclaring variables +let duplicate = 1; +let duplicate = 2; + +// Dead code +function deadCode() { + return 5; + console.log('This will never run'); +} + +// Empty block +if (true) { +} + +// Debugger statement +function debugging() { + debugger; + return 'remove debugger'; +} + +// Alert statement +function showAlert() { + alert('Remove alerts from production code'); +} + +// Console.log in production code +function logData(data) { + console.log('Debug:', data); + return data; +} + +// Eval usage (security issue) +function dangerous(code) { + return eval(code); +} + +// With statement (deprecated) +function useWith(obj) { + with (obj) { + console.log(prop); + } +} + +// No return in function that should return +function shouldReturn(x) { + if (x > 0) { + x * 2; + } +} + +// Modifying parameters +function modifyParam(obj) { + obj.modified = true; + return obj; +} + +// Long line +const veryLongLine = "This is a very very very very very very very very very very very very very very very very very very long line that should be broken up"; + +// Multiple statements on one line +function multiple() { const a = 1; const b = 2; return a + b; } + +// Missing radix in parseInt +const parsed = parseInt("10"); + +// new Array() instead of literal +const arr = new Array(1, 2, 3); + +// new Object() instead of literal +const obj = new Object(); \ No newline at end of file diff --git a/ash_test_files/bad_python_code.py b/ash_test_files/bad_python_code.py new file mode 100644 index 000000000..02cf70984 --- /dev/null +++ b/ash_test_files/bad_python_code.py @@ -0,0 +1,93 @@ +"""Python code with various linting issues for testing.""" + +import os, sys, json +import time +from typing import List + +# Missing docstring +def calculate_sum(a,b,c): + result=a+b+c # Bad spacing + return result + +class poorly_formatted: # Class name not CamelCase + def __init__(self,x,y): + self.x=x + self.y=y + self.z = None # Inconsistent spacing + + # Missing docstring + def DoSomething(self): # Method not snake_case + pass + +# Line too long +def function_with_extremely_long_line(): + really_really_really_really_really_really_really_really_really_really_long_variable_name = "This is a very very very very very very very long string that definitely exceeds the recommended line length" + return really_really_really_really_really_really_really_really_really_really_long_variable_name + +# Unused imports and variables +def function_with_unused(): + unused_var = 42 + another_unused = "test" + result = 10 + 20 + return result + +# Missing type hints +def add_numbers(x, y): + return x + y + +# Comparison with None using == +def check_value(val): + if val == None: # Should use 'is None' + return False + elif val != None: # Should use 'is not None' + return True + +# Using bare except +def risky_function(): + try: + x = 1 / 0 + except: # Bare except + pass + +# Mutable default argument +def append_item(item, list_arg=[]): + list_arg.append(item) + return list_arg + +# Global variable modification +counter = 0 + +def increment(): + global counter + counter = counter + 1 + return counter + +# Unnecessary else after return +def check_positive(n): + if n > 0: + return True + else: # Unnecessary + return False + +# Import not at top +def late_import(): + import datetime # Import should be at top + return datetime.datetime.now() + +# Trailing whitespace and tabs +def mixed_indentation(): + x = 1 # Tab indentation + y = 2 # Space indentation + return x + y # Mixed + +# Multiple statements on one line +def multiple_statements(): x = 1; y = 2; return x + y + +# Wildcard import +from os import * # Avoid wildcard imports + +# Redefinition of built-in +def list(): # Shadows built-in + return [] + +# Missing final newline \ No newline at end of file diff --git a/ash_test_files/security_vulnerabilities.js b/ash_test_files/security_vulnerabilities.js new file mode 100644 index 000000000..98874a9e0 --- /dev/null +++ b/ash_test_files/security_vulnerabilities.js @@ -0,0 +1,173 @@ +/** + * JavaScript file with security vulnerabilities for testing + */ + +// SQL Injection vulnerability +function getUserData(userId) { + // Never concatenate user input into SQL queries + const query = `SELECT * FROM users WHERE id = ${userId}`; + // db.query(query); // Would execute unsafe query + return query; +} + +// Command injection vulnerability +function runCommand(filename) { + const { exec } = require('child_process'); + // Never pass user input directly to exec + exec(`cat ${filename}`, (error, stdout, stderr) => { + console.log(stdout); + }); +} + +// Hardcoded credentials +const config = { + databasePassword: 'admin123', // Never hardcode passwords + apiKey: 'sk-1234567890abcdefghijklmnop', // Never hardcode API keys + secretToken: 'secret_token_12345', // Never hardcode tokens + awsAccessKey: 'AKIAIOSFODNN7EXAMPLE', // AWS credentials + awsSecretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' +}; + +// Eval with user input (code injection) +function evaluateExpression(expr) { + // eval executes arbitrary JavaScript code + return eval(expr); +} + +// Unsafe regex (ReDoS vulnerability) +function validateEmail(email) { + // Catastrophic backtracking possible + const regex = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/; + return regex.test(email); +} + +// XSS vulnerability - direct HTML injection +function displayUserContent(userInput) { + // Never insert user input directly into HTML + document.getElementById('content').innerHTML = userInput; +} + +// Insecure random number generation +function generateToken() { + // Math.random() is not cryptographically secure + return Math.floor(Math.random() * 1000000); +} + +// Path traversal vulnerability +function readFile(filename) { + const fs = require('fs'); + // No validation allows ../../etc/passwd + const path = `/var/data/${filename}`; + return fs.readFileSync(path, 'utf8'); +} + +// Prototype pollution vulnerability +function mergeObjects(target, source) { + // Vulnerable to prototype pollution + for (const key in source) { + if (typeof source[key] === 'object') { + target[key] = mergeObjects(target[key] || {}, source[key]); + } else { + target[key] = source[key]; + } + } + return target; +} + +// Using deprecated/vulnerable functions +function hashPassword(password) { + const crypto = require('crypto'); + // MD5 is cryptographically broken + return crypto.createHash('md5').update(password).digest('hex'); +} + +// Open redirect vulnerability +function redirectUser(url) { + // No validation allows redirect to malicious sites + window.location.href = url; +} + +// localStorage with sensitive data +function storeSensitiveData(creditCard, ssn) { + // Never store sensitive data in localStorage + localStorage.setItem('creditCard', creditCard); + localStorage.setItem('ssn', ssn); +} + +// CORS misconfiguration +function setupCORS(req, res) { + // Allows any origin - security risk + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); +} + +// Timing attack vulnerability +function comparePasswords(input, actual) { + // Early return creates timing differences + if (input.length !== actual.length) { + return false; + } + + for (let i = 0; i < input.length; i++) { + if (input[i] !== actual[i]) { + return false; // Early return leaks information + } + } + return true; +} + +// Unsafe deserialization +function deserializeData(data) { + // Function constructor can execute arbitrary code + return new Function('return ' + data)(); +} + +// Information disclosure in error messages +function loginUser(username, password) { + if (username === 'admin' && password !== 'correct') { + // Reveals that 'admin' is a valid username + throw new Error('Invalid password for admin user'); + } +} + +// Client-side authentication +function checkAuth() { + // Never do authentication on client side only + const isAdmin = document.cookie.includes('admin=true'); + if (isAdmin) { + showAdminPanel(); + } +} + +// Weak session management +function createSession(userId) { + // Predictable session ID + const sessionId = userId + '_' + Date.now(); + document.cookie = `session=${sessionId}`; +} + +// NoSQL injection +function findUser(username) { + // Vulnerable to NoSQL injection with MongoDB + const query = { username: username }; + // db.users.find(query); + return query; +} + +// XXE in XML parsing +function parseXML(xmlString) { + const parser = new DOMParser(); + // Default parser may be vulnerable to XXE + const xmlDoc = parser.parseFromString(xmlString, "text/xml"); + return xmlDoc; +} + +// Export for testing +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + getUserData, + evaluateExpression, + generateToken, + config + }; +} \ No newline at end of file diff --git a/ash_test_files/security_vulnerabilities.py b/ash_test_files/security_vulnerabilities.py new file mode 100644 index 000000000..f8f45185e --- /dev/null +++ b/ash_test_files/security_vulnerabilities.py @@ -0,0 +1,159 @@ +"""Python file with security vulnerabilities for ASH and CodeQL testing.""" + +import os +import pickle +import subprocess +import hashlib +import random +from typing import Any + + +# SQL Injection vulnerability +def get_user_data(user_id: str): + """Vulnerable to SQL injection.""" + # Never use string formatting for SQL queries + query = f"SELECT * FROM users WHERE id = {user_id}" + # In real code, would execute: cursor.execute(query) + return query + + +# Command injection vulnerability +def run_system_command(filename: str): + """Vulnerable to command injection.""" + # Never use shell=True with user input + result = subprocess.run( + f"cat {filename}", + shell=True, + capture_output=True, + text=True + ) + return result.stdout + + +# Hardcoded credentials (security issue) +def connect_to_database(): + """Contains hardcoded credentials.""" + DATABASE_PASSWORD = "admin123" # Never hardcode passwords + API_KEY = "sk-1234567890abcdefghijklmnop" # Never hardcode API keys + SECRET_TOKEN = "secret_token_12345" # Never hardcode tokens + + connection_string = f"postgresql://admin:{DATABASE_PASSWORD}@localhost/db" + return connection_string + + +# Unsafe deserialization +def load_user_data(serialized_data: bytes) -> Any: + """Vulnerable to unsafe deserialization attacks.""" + # pickle.loads can execute arbitrary code + return pickle.loads(serialized_data) + + +# Path traversal vulnerability +def read_file(filename: str): + """Vulnerable to path traversal attacks.""" + # No input validation allows ../../../etc/passwd + filepath = f"/var/data/{filename}" + with open(filepath, 'r') as f: + return f.read() + + +# Weak cryptography +def hash_password(password: str): + """Uses weak hashing algorithm.""" + # MD5 is cryptographically broken, never use for passwords + return hashlib.md5(password.encode()).hexdigest() + + +# Insecure random number generation +def generate_token(): + """Uses insecure random for security-sensitive operation.""" + # random module is not cryptographically secure + token = random.randint(100000, 999999) + return str(token) + + +# Eval with user input (code injection) +def calculate_expression(expr: str): + """Vulnerable to code injection via eval.""" + # eval() executes arbitrary Python code + result = eval(expr) + return result + + +# XXE vulnerability in XML parsing +def parse_xml(xml_string: str): + """Vulnerable to XML External Entity attacks.""" + import xml.etree.ElementTree as ET + # Default XML parser is vulnerable to XXE + root = ET.fromstring(xml_string) + return root + + +# SSRF vulnerability +def fetch_url(url: str): + """Vulnerable to Server-Side Request Forgery.""" + import urllib.request + # No validation allows internal network access + response = urllib.request.urlopen(url) + return response.read() + + +# Insecure temporary file +def create_temp_file(data: str): + """Creates predictable temporary file.""" + # Predictable filename in shared directory + temp_file = f"/tmp/app_temp_{os.getpid()}.txt" + with open(temp_file, 'w') as f: + f.write(data) + return temp_file + + +# Using assert for security (gets removed in optimized code) +def validate_admin(user_role: str): + """Incorrectly uses assert for security check.""" + # assert statements are removed with python -O + assert user_role == "admin", "Unauthorized access" + return True + + +# Logging sensitive information +def process_payment(card_number: str, cvv: str): + """Logs sensitive payment information.""" + import logging + # Never log sensitive data + logging.info(f"Processing payment for card: {card_number}, CVV: {cvv}") + return "Payment processed" + + +# Race condition vulnerability +COUNTER = 0 + +def increment_counter(): + """Has race condition in multi-threaded environment.""" + global COUNTER + # Not thread-safe, can lead to race conditions + temp = COUNTER + temp = temp + 1 + COUNTER = temp + return COUNTER + + +# Open redirect vulnerability +def redirect_user(next_url: str): + """Vulnerable to open redirect attacks.""" + # No validation allows redirect to malicious sites + return f"Location: {next_url}" + + +if __name__ == "__main__": + # Example of unsafe operations (DO NOT RUN) + print("This file contains intentional security vulnerabilities") + print("For testing ASH and CodeQL security scanning only") + + # These would be flagged by security scanners + password = "weak_password" + weak_hash = hash_password(password) + print(f"Weak hash: {weak_hash}") + + # Hardcoded secret in string + secret = "aws_secret_access_key_AKIAIOSFODNN7EXAMPLE" \ No newline at end of file