Skip to content

fix(webapp): קישורי תפריט פנימיים במסמכי Markdown לא גללו לסעיף #6184

fix(webapp): קישורי תפריט פנימיים במסמכי Markdown לא גללו לסעיף

fix(webapp): קישורי תפריט פנימיים במסמכי Markdown לא גללו לסעיף #6184

Workflow file for this run

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