diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c5eea9f86..9e0e8cb5c 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -141,13 +141,49 @@ jobs: path: pip-audit-results.json retention-days: 30 - - name: Create issue on vulnerability - if: steps.pip-audit.outputs.vulnerabilities_found == 'true' + - name: Create or close pip security issue + if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); - const results = JSON.parse(fs.readFileSync('pip-audit-results.json', 'utf8')); + + // Check for existing open issue + const existingIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'security,automated' + }); + const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('Python')); + + // If no vulnerabilities found, auto-close any stale issue + if ('${{ steps.pip-audit.outputs.vulnerabilities_found }}' !== 'true') { + if (existingIssue) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: 'All Python vulnerabilities have been resolved. Closing automatically.' + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + state: 'closed' + }); + console.log(`Auto-closed resolved issue #${existingIssue.number}`); + } + return; + } + + let results; + try { + results = JSON.parse(fs.readFileSync('pip-audit-results.json', 'utf8')); + } catch { + console.log('Could not read audit results'); + return; + } // Build vulnerability table let table = '| Package | Version | Vulnerability | Fix Version |\n'; @@ -159,17 +195,29 @@ jobs: } } - const title = `Security Alert: ${results.dependencies?.reduce((acc, d) => acc + (d.vulns?.length || 0), 0) || 0} Python vulnerabilities found`; + const vulnCount = results.dependencies?.reduce((acc, d) => acc + (d.vulns?.length || 0), 0) || 0; - // Check for existing open issue - const existingIssues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: 'security,automated' - }); + if (vulnCount === 0) { + console.log('No vulnerabilities to report'); + if (existingIssue) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: 'All Python vulnerabilities have been resolved. Closing automatically.' + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + state: 'closed' + }); + console.log(`Auto-closed resolved issue #${existingIssue.number}`); + } + return; + } - const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('Python')); + const title = `Security Alert: ${vulnCount} Python vulnerabilities found`; const body = `## Automated Security Audit Results @@ -182,7 +230,6 @@ jobs: 1. Review each vulnerability 2. Update affected packages: \`pip install --upgrade \` 3. Run \`pip-audit\` locally to verify fixes - 4. Close this issue when resolved --- *This issue was automatically created by the security audit workflow.*`; @@ -230,12 +277,14 @@ jobs: id: npm-audit working-directory: frontend run: | - npm audit --omit=dev --json > npm-audit-raw.json 2>&1 || true + npm audit --omit=dev --json > npm-audit-raw.json 2>/dev/null || true # Filter audit results to only include actual project dependencies # (npm audit sometimes reports vulnerabilities in the npm CLI itself) node -e " const fs = require('fs'); - const results = JSON.parse(fs.readFileSync('npm-audit-raw.json', 'utf8')); + const raw = fs.readFileSync('npm-audit-raw.json', 'utf8'); + let results; + try { results = JSON.parse(raw); } catch { results = { vulnerabilities: {} }; } const depTree = JSON.parse(require('child_process').execSync( 'npm ls --omit=dev --all --json 2>/dev/null', { encoding: 'utf8' } )); @@ -269,19 +318,70 @@ jobs: path: frontend/npm-audit-results.json retention-days: 30 - - name: Create issue on vulnerability - if: steps.npm-audit.outputs.vulnerabilities_found == 'true' + - name: Create or close npm security issue + if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); - const results = JSON.parse(fs.readFileSync('frontend/npm-audit-results.json', 'utf8')); + + // Check for existing open issue + const existingIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'security,automated' + }); + const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('npm')); + + // If filter didn't flag vulnerabilities, auto-close any stale issue + if ('${{ steps.npm-audit.outputs.vulnerabilities_found }}' !== 'true') { + if (existingIssue) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: 'All npm production vulnerabilities have been resolved. Closing automatically.' + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + state: 'closed' + }); + console.log(`Auto-closed resolved issue #${existingIssue.number}`); + } + return; + } + + let results; + try { + results = JSON.parse(fs.readFileSync('frontend/npm-audit-results.json', 'utf8')); + } catch { + console.log('Could not read filtered audit results'); + return; + } const vulns = results.vulnerabilities || {}; const vulnCount = Object.keys(vulns).length; if (vulnCount === 0) { console.log('No vulnerabilities to report'); + if (existingIssue) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: 'All npm production vulnerabilities have been resolved. Closing automatically.' + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + state: 'closed' + }); + console.log(`Auto-closed resolved issue #${existingIssue.number}`); + } return; } @@ -296,16 +396,6 @@ jobs: const title = `Security Alert: ${vulnCount} npm vulnerabilities found`; - // Check for existing open issue - const existingIssues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: 'security,automated' - }); - - const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('npm')); - const body = `## Automated Security Audit Results The weekly security audit found vulnerabilities in npm dependencies. @@ -317,7 +407,6 @@ jobs: 1. Review each vulnerability: \`npm audit\` 2. Auto-fix if possible: \`npm audit fix\` 3. Manual fix for breaking changes: \`npm audit fix --force\` (review changes!) - 4. Close this issue when resolved --- *This issue was automatically created by the security audit workflow.*`;