fix(webapp): קישורי תפריט פנימיים במסמכי Markdown לא גללו לסעיף #6184
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 Scan" | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| schedule: | |
| - cron: '0 2 1 * *' | |
| workflow_dispatch: | |
| concurrency: | |
| group: security-scan-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| security-scan: | |
| name: "🛡️ Security Analysis (heavy)" | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| permissions: | |
| security-events: write | |
| contents: read | |
| issues: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ['3.12'] | |
| env: | |
| TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} | |
| TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Run CodeQL early to ensure configuration is detected on PRs | |
| - name: "🔍 CodeQL Analysis" | |
| uses: github/codeql-action/init@v3 | |
| with: | |
| languages: python | |
| continue-on-error: true | |
| - name: "🔍 Perform CodeQL Analysis" | |
| uses: github/codeql-action/analyze@v3 | |
| continue-on-error: true | |
| - name: 🐍 Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| cache: 'pip' | |
| cache-dependency-path: | | |
| requirements/*.txt | |
| - name: 📦 Install deps for audits | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements/production.txt | |
| pip freeze | sort > constraints.txt | |
| pip install -r requirements/production.txt -c constraints.txt | |
| pip install bandit safety pip-audit | |
| - name: "🔎 Debug Telegram notify config" | |
| run: | | |
| echo "Has TELEGRAM_BOT_TOKEN: $([ -n "${{ env.TELEGRAM_BOT_TOKEN }}" ] && echo yes || echo no)" | |
| echo "Has TELEGRAM_CHAT_ID: $([ -n "${{ env.TELEGRAM_CHAT_ID }}" ] && echo yes || echo no)" | |
| - name: "🧪 Trivy FS scan (repo)" | |
| uses: aquasecurity/trivy-action@v0.35.0 | |
| with: | |
| scan-type: fs | |
| ignore-unfixed: true | |
| format: 'json' | |
| exit-code: '0' | |
| severity: 'CRITICAL,HIGH' | |
| scanners: 'vuln,secret,config' | |
| output: trivy-fs.json | |
| continue-on-error: true | |
| - name: "🧪 Trivy FS scan (SARIF)" | |
| uses: aquasecurity/trivy-action@v0.35.0 | |
| with: | |
| scan-type: fs | |
| ignore-unfixed: true | |
| format: 'sarif' | |
| exit-code: '0' | |
| severity: 'CRITICAL,HIGH' | |
| scanners: 'vuln,secret,config' | |
| output: trivy-fs.sarif | |
| continue-on-error: true | |
| - name: "📊 Upload SARIF (Trivy FS)" | |
| if: always() && github.event_name != 'pull_request' && hashFiles('trivy-fs.sarif') != '' | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: trivy-fs.sarif | |
| category: trivy-fs | |
| - name: "🛠️ Build app image (production)" | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| docker build --pull --no-cache -t app:scan --target production . | |
| - name: "🧪 Trivy Image scan (app:scan)" | |
| if: github.event_name != 'pull_request' | |
| uses: aquasecurity/trivy-action@v0.35.0 | |
| with: | |
| image-ref: 'app:scan' | |
| ignore-unfixed: true | |
| format: 'json' | |
| exit-code: '0' | |
| severity: 'CRITICAL,HIGH' | |
| output: trivy-image.json | |
| continue-on-error: true | |
| - name: "🧪 Trivy Image scan (SARIF)" | |
| if: github.event_name != 'pull_request' | |
| uses: aquasecurity/trivy-action@v0.35.0 | |
| with: | |
| image-ref: 'app:scan' | |
| ignore-unfixed: true | |
| format: 'sarif' | |
| exit-code: '0' | |
| severity: 'CRITICAL,HIGH' | |
| output: trivy-image.sarif | |
| continue-on-error: true | |
| - name: "📊 Upload SARIF (Trivy Image)" | |
| if: always() && github.event_name != 'pull_request' && hashFiles('trivy-image.sarif') != '' | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: trivy-image.sarif | |
| category: trivy-image | |
| - name: "🧪 Safety check" | |
| run: safety check --full-report || true | |
| - name: "📦 pip-audit" | |
| run: | | |
| pip-audit -r requirements/production.txt -f json -o pip-audit.json || true | |
| - name: Upload pip-audit report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pip-audit | |
| path: pip-audit.json | |
| if-no-files-found: ignore | |
| - name: Upload Trivy reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: trivy-reports | |
| path: | | |
| trivy-fs.json | |
| trivy-image.json | |
| if-no-files-found: ignore | |
| - name: "📣 Post scan summary as Issue (main, scheduled/manual only)" | |
| id: scan_summary | |
| uses: actions/github-script@v7 | |
| if: github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | |
| continue-on-error: true | |
| with: | |
| result-encoding: string | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const link = `https://github.com/${owner}/${repo}/security/code-scanning`; | |
| const fs = require('fs'); | |
| // Parse Trivy JSONs to summarize severities | |
| function summarizeTrivy(jsonStr) { | |
| const counts = { Critical: 0, High: 0, Medium: 0, Low: 0, Unknown: 0 }; | |
| if (!jsonStr) return counts; | |
| try { | |
| const data = JSON.parse(jsonStr); | |
| const results = Array.isArray(data.Results) ? data.Results : []; | |
| for (const res of results) { | |
| const vulns = Array.isArray(res.Vulnerabilities) ? res.Vulnerabilities : []; | |
| for (const v of vulns) { | |
| const sev = String(v.Severity || '').toLowerCase(); | |
| if (sev === 'critical') counts.Critical += 1; | |
| else if (sev === 'high') counts.High += 1; | |
| else if (sev === 'medium') counts.Medium += 1; | |
| else if (sev === 'low') counts.Low += 1; | |
| else counts.Unknown += 1; | |
| } | |
| } | |
| } catch (e) { | |
| // ignore parse errors | |
| } | |
| return counts; | |
| } | |
| let fsCounts = { Critical: 0, High: 0, Medium: 0, Low: 0, Unknown: 0 }; | |
| let imgCounts = { Critical: 0, High: 0, Medium: 0, Low: 0, Unknown: 0 }; | |
| try { fsCounts = summarizeTrivy(fs.readFileSync('trivy-fs.json', 'utf8')); } catch(e) {} | |
| try { imgCounts = summarizeTrivy(fs.readFileSync('trivy-image.json', 'utf8')); } catch(e) {} | |
| let alerts = []; | |
| let mdBody = ''; | |
| let htmlBody = ''; | |
| let defaultBranch = ''; | |
| let latestCommit = null; | |
| try { | |
| const repoInfo = await github.rest.repos.get({ owner, repo }); | |
| defaultBranch = repoInfo.data.default_branch || 'main'; | |
| const commits = await github.rest.repos.listCommits({ owner, repo, sha: defaultBranch, per_page: 1 }); | |
| latestCommit = commits.data && commits.data[0] ? commits.data[0] : null; | |
| } catch (e) { | |
| core.warning(`Failed to fetch default branch/commit: ${e.message}`); | |
| } | |
| try { | |
| alerts = await github.paginate( | |
| github.rest.codeScanning.listAlertsForRepo, | |
| { owner, repo, state: 'open', per_page: 100 } | |
| ); | |
| const sevCounts = { critical: 0, high: 0, medium: 0, low: 0, warning: 0, error: 0, note: 0, unknown: 0 }; | |
| for (const a of alerts) { | |
| const sev = (a.rule && (a.rule.security_severity_level || a.rule.severity)) || a.severity || 'unknown'; | |
| const key = String(sev).toLowerCase(); | |
| if (Object.prototype.hasOwnProperty.call(sevCounts, key)) sevCounts[key] += 1; else sevCounts.unknown += 1; | |
| } | |
| const total = alerts.length; | |
| const commitSha = latestCommit?.sha ? latestCommit.sha.substring(0, 7) : '—'; | |
| const commitUrl = latestCommit?.html_url || ''; | |
| const commitAuthor = latestCommit?.author?.login || latestCommit?.commit?.author?.name || '—'; | |
| const commitDate = latestCommit?.commit?.author?.date ? new Date(latestCommit.commit.author.date).toISOString().slice(0,16).replace('T',' ') + 'Z' : '—'; | |
| const commitMsg = latestCommit?.commit?.message?.split('\n')[0] || ''; | |
| mdBody = [ | |
| `🛡️ Security Scan Summary`, | |
| `\n\nRepository: ${owner}/${repo}`, | |
| `\nDefault branch: ${defaultBranch || '—'}`, | |
| commitUrl ? `\nLatest commit: [${commitSha}](${commitUrl}) by @${commitAuthor} on ${commitDate} — ${commitMsg}` : `\nLatest commit: ${commitSha} by @${commitAuthor} on ${commitDate} — ${commitMsg}`, | |
| `\n\nLink: [Code Scanning](${link})`, | |
| `\n\nOpen alerts summary (CodeQL):`, | |
| `\n- Critical: ${sevCounts.critical}`, | |
| `\n- High: ${sevCounts.high}`, | |
| `\n- Medium: ${sevCounts.medium}`, | |
| `\n- Low: ${sevCounts.low}`, | |
| `\n- Warning: ${sevCounts.warning}`, | |
| `\n- Error: ${sevCounts.error}`, | |
| `\n- Note: ${sevCounts.note}`, | |
| `\n- Unknown: ${sevCounts.unknown}`, | |
| `\n\nTotal (CodeQL alerts): ${total}`, | |
| `\n\nTrivy FS vuln summary (repo):`, | |
| `\n- Critical: ${fsCounts.Critical}`, | |
| `\n- High: ${fsCounts.High}`, | |
| `\n- Medium: ${fsCounts.Medium}`, | |
| `\n- Low: ${fsCounts.Low}`, | |
| `\n- Unknown: ${fsCounts.Unknown}`, | |
| `\n\nTrivy Image vuln summary (app:scan):`, | |
| `\n- Critical: ${imgCounts.Critical}`, | |
| `\n- High: ${imgCounts.High}`, | |
| `\n- Medium: ${imgCounts.Medium}`, | |
| `\n- Low: ${imgCounts.Low}`, | |
| `\n- Unknown: ${imgCounts.Unknown}`, | |
| `\n\n(Full JSON reports are uploaded as workflow artifacts: trivy-reports)` | |
| ].join(''); | |
| htmlBody = [ | |
| `<b>🛡️ Security Scan Summary</b>`, | |
| `<br/><br/>Repository: <code>${owner}/${repo}</code>`, | |
| `<br/>Default branch: <code>${defaultBranch || '—'}</code>`, | |
| latestCommit ? `<br/>Latest commit: <a href="${commitUrl}"><code>${commitSha}</code></a> by <b>@${commitAuthor}</b> on ${commitDate} — ${commitMsg}` : '', | |
| `<br/><br/>Link: <a href="${link}">Code Scanning</a>`, | |
| `<br/><br/><b>Open alerts summary (CodeQL):</b>`, | |
| `<br/>• Critical: ${sevCounts.critical}`, | |
| `<br/>• High: ${sevCounts.high}`, | |
| `<br/>• Medium: ${sevCounts.medium}`, | |
| `<br/>• Low: ${sevCounts.low}`, | |
| `<br/>• Warning: ${sevCounts.warning}`, | |
| `<br/>• Error: ${sevCounts.error}`, | |
| `<br/>• Note: ${sevCounts.note}`, | |
| `<br/>• Unknown: ${sevCounts.unknown}`, | |
| `<br/><br/>Total (CodeQL alerts): <b>${total}</b>`, | |
| `<br/><br/><b>Trivy FS vuln summary (repo):</b>`, | |
| `<br/>• Critical: ${fsCounts.Critical}`, | |
| `<br/>• High: ${fsCounts.High}`, | |
| `<br/>• Medium: ${fsCounts.Medium}`, | |
| `<br/>• Low: ${fsCounts.Low}`, | |
| `<br/>• Unknown: ${fsCounts.Unknown}`, | |
| `<br/><br/><b>Trivy Image vuln summary (app:scan):</b>`, | |
| `<br/>• Critical: ${imgCounts.Critical}`, | |
| `<br/>• High: ${imgCounts.High}`, | |
| `<br/>• Medium: ${imgCounts.Medium}`, | |
| `<br/>• Low: ${imgCounts.Low}`, | |
| `<br/>• Unknown: ${imgCounts.Unknown}`, | |
| `<br/><br/>(Full JSON reports are uploaded as artifacts: trivy-reports)` | |
| ].join(''); | |
| } catch (err) { | |
| core.warning(`Failed to fetch Code Scanning alerts: ${err.message}`); | |
| mdBody = [ | |
| `🛡️ Security Scan Summary`, | |
| `\n\nRepository: ${owner}/${repo}`, | |
| `\n\nLink: [Code Scanning](${link})`, | |
| `\n\nלא ניתן היה למשוך התראות דרך ה‑API. בדקו ידנית בלשונית Security.` | |
| ].join(''); | |
| htmlBody = [ | |
| `<b>🛡️ Security Scan Summary</b>`, | |
| `<br/><br/>Repository: <code>${owner}/${repo}</code>`, | |
| `<br/><br/>Link: <a href="${link}">Code Scanning</a>`, | |
| `<br/><br/>לא ניתן היה למשוך התראות דרך ה‑API. בדקו ידנית בלשונית Security.` | |
| ].join(''); | |
| } | |
| const baseTitle = '🛡️ Security Scan Summary'; | |
| // Upsert: update existing open issue with the base title (or create if missing) | |
| let targetIssue = null; | |
| try { | |
| const existing = await github.rest.issues.listForRepo({ owner, repo, state: 'open', per_page: 100, labels: undefined }); | |
| targetIssue = (existing.data || []).find(iss => (iss.title || '') === baseTitle) || null; | |
| } catch (e) { | |
| core.warning(`Failed to list issues: ${e.message}`); | |
| } | |
| if (targetIssue) { | |
| await github.rest.issues.update({ owner, repo, issue_number: targetIssue.number, body: mdBody }); | |
| } else { | |
| await github.rest.issues.create({ owner, repo, title: baseTitle, body: mdBody, labels: ['documentation','security','automated'] }); | |
| } | |
| core.setOutput('html', htmlBody); | |
| core.setOutput('text', mdBody); | |
| return mdBody; | |
| - name: "📨 Notify Telegram" | |
| if: ${{ env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' && github.ref == 'refs/heads/main' && github.event_name == 'schedule' }} | |
| run: | | |
| printf '%s' "${{ steps.scan_summary.outputs.text }}" | head -c 3500 | \ | |
| curl -sS -o /tmp/tg.json -w "%{http_code}" -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ | |
| -d "chat_id=${TELEGRAM_CHAT_ID}" \ | |
| --data-urlencode "text@-" || true |