Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
1f9922a
feat(action): post branded PR comments via backend webhook (PPSC-556)
yiftach-armis Jun 18, 2026
127a7fa
fix(action): wire api-url into scan step; correct HTTPS comment (PPSC…
yiftach-armis Jun 18, 2026
01592cb
fix(action): document SARIF requirement for pr-comment; tidy temp fil…
yiftach-armis Jun 18, 2026
98d7e06
fix(action): escape workflow-cmd chars in warn(); fix region URL mapp…
yiftach-armis Jul 12, 2026
11ea532
fix(action): fall back to primary host for unknown regions (PPSC-556)
yiftach-armis Jul 12, 2026
a6f9231
fix(action): use bash substitution for workflow-cmd escaping; also es…
yiftach-armis Jul 12, 2026
cbd7201
fix(action): portable mktemp templates for BSD/macOS compatibility (P…
yiftach-armis Jul 12, 2026
1b6f496
fix(action): preflight jq/curl check; capture token-exchange HTTP sta…
yiftach-armis Jul 12, 2026
bd01c45
fix(action): clarify pr-comment event restriction in docs; sanitize c…
yiftach-armis Jul 12, 2026
d3ea01d
fix(action): correct fail-on default description — empty uses CLI def…
yiftach-armis Jul 12, 2026
8d37e1f
fix(action): drop pull-requests:write permission; log actual BASE_URL…
yiftach-armis Jul 12, 2026
42c3010
fix(action): use jq $ENV for client secret to avoid argv exposure (PP…
yiftach-armis Jul 12, 2026
24e87d1
fix(action): fall back to ARMIS_REGION env var in pr-comment step (PP…
yiftach-armis Jul 12, 2026
4a73ffd
fix(action): strip CR/LF from api-token before Authorization header (…
yiftach-armis Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 8 additions & 195 deletions .github/workflows/reusable-security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ on:

# Top-level permissions define the maximum permissions available to this workflow.
# Job-level permissions further restrict as needed.
# pull-requests: write was removed in PPSC-556: PR commenting is now handled by
# the Armis backend GitHub App (armis-appsec[bot]) and no longer requires the
# workflow's GITHUB_TOKEN to have PR write access.
permissions:
contents: read
security-events: write
actions: read
pull-requests: write

jobs:
security-scan:
Expand All @@ -82,7 +84,6 @@ jobs:
contents: read
security-events: write
actions: read
pull-requests: write

steps:
- name: Checkout repository
Expand All @@ -108,6 +109,7 @@ jobs:
scan-timeout: ${{ inputs.scan-timeout }}
include-files: ${{ inputs.include-files }}
build-from-source: ${{ inputs.build-from-source }}
pr-comment: ${{ inputs.pr-comment && github.event_name == 'pull_request' }}
continue-on-error: true

- name: Ensure SARIF exists
Expand All @@ -117,199 +119,10 @@ jobs:
echo '{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"armis-cli","version":"1.0.0"}},"results":[]}]}' > armis-results.sarif
fi

- name: Post PR Comment with Results
if: always() && inputs.pr-comment && github.event_name == 'pull_request'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const fs = require('fs');

// Build a map of {filename: Set<lineNumber>} for lines visible in the PR diff.
// These are the only lines GitHub Code Scanning annotates inline (added/context lines).
// Only '+' (added) and ' ' (context) lines are included; '-' (removed) lines are skipped
// because they don't exist in the new file. Lines starting with '\' (e.g. "\ No newline
// at end of file") are also skipped to avoid misaligning the computed line numbers.
function parseDiffLineNumbers(patch) {
const lines = new Set();
if (!patch) return lines;
let currentLine = 0;
for (const raw of patch.split('\n')) {
const m = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (m) { currentLine = parseInt(m[1], 10); continue; }
if (currentLine === 0) continue;
if (raw.startsWith('-') || raw.startsWith('\\')) continue;
if (raw.startsWith('+') || raw.startsWith(' ')) {
lines.add(currentLine++);
}
}
return lines;
}

// Use a null-prototype object to avoid prototype pollution from filenames
// like '__proto__' or 'constructor' that would corrupt a regular object.
// null sentinel means the file is changed but has no patch (e.g. large diffs
// or binary files) — findings for those files are always included so we never
// silently under-report issues on files GitHub couldn't supply a patch for.
const diffLines = Object.create(null);
let page = 1;
while (true) {
const { data: files } = await github.rest.pulls.listFiles({
...context.repo,
pull_number: context.issue.number,
per_page: 100,
page,
});
if (!files.length) break;
for (const f of files) {
diffLines[f.filename] = f.patch ? parseDiffLineNumbers(f.patch) : null;
}
if (files.length < 100) break;
page++;
}

// Read SARIF results
const sarif = JSON.parse(fs.readFileSync('armis-results.sarif', 'utf8'));
const allResults = sarif.runs?.[0]?.results || [];

// Keep only findings whose (file, line) falls on a diff-visible line —
// these are the only ones GitHub Code Scanning will annotate inline.
// Findings for files with no patch data (null sentinel) are always kept
// to avoid under-reporting on large or binary files.
const results = allResults.filter(r => {
const file = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri || '';
const line = r.locations?.[0]?.physicalLocation?.region?.startLine;
if (!file || !line) return false;
if (!(file in diffLines)) return false;
const lineSet = diffLines[file];
return lineSet === null || lineSet.has(line);
});

// Count by severity - read from properties.severity if available
const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, INFO: 0 };
for (const r of results) {
// Prefer properties.severity (set by armis-cli), fallback to level mapping
const severity = r.properties?.severity || {
'error': 'HIGH',
'warning': 'MEDIUM',
'note': 'LOW',
'none': 'INFO'
}[r.level || 'warning'] || 'INFO';
counts[severity]++;
}
const total = results.length;

// Build comment body
const marker = '<!-- armis-security-scan -->';
const status = counts.CRITICAL > 0 ? '🔴 CRITICAL issues found' :
counts.HIGH > 0 ? '🟠 HIGH issues found' :
total > 0 ? '🟡 Issues found' : '✅ No issues';

const workflowRef = process.env.GITHUB_WORKFLOW_REF || '';
const ref = workflowRef.split('@')[1] || 'main';
const logoLight = `https://raw.githubusercontent.com/ArmisSecurity/armis-cli/${ref}/docs/assets/appsec-logo-light.png`;
const logoDark = `https://raw.githubusercontent.com/ArmisSecurity/armis-cli/${ref}/docs/assets/appsec-logo-dark.png`;
// GitHub theme-aware images with height constraint (24px to match heading text)
const logo = `<img alt="Armis AppSec" src="${logoLight}#gh-dark-mode-only" height="24"><img alt="Armis AppSec" src="${logoDark}#gh-light-mode-only" height="24">`;

let body = `${marker}\n## ${logo}&nbsp;Security Scan Results\n\n**${status}**\n`;
if (total > 0) {
body += `\n| Severity | Count |\n|----------|-------|\n`;
if (counts.CRITICAL > 0) body += `| 🔴 CRITICAL | ${counts.CRITICAL} |\n`;
if (counts.HIGH > 0) body += `| 🟠 HIGH | ${counts.HIGH} |\n`;
if (counts.MEDIUM > 0) body += `| 🟡 MEDIUM | ${counts.MEDIUM} |\n`;
if (counts.LOW > 0) body += `| 🔵 LOW | ${counts.LOW} |\n`;
if (counts.INFO > 0) body += `| ⚪ INFO | ${counts.INFO} |\n`;
body += `\n**Total: ${total}**\n`;
}

// Build detailed findings section
if (total > 0) {
body += `\n<details><summary>View all ${total} findings</summary>\n\n`;

// Group results by severity
const severityOrder = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
const severityEmoji = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵', INFO: '⚪' };

for (const severity of severityOrder) {
const severityResults = results.filter(r =>
(r.properties?.severity || 'INFO') === severity
);

if (severityResults.length > 0) {
body += `### ${severityEmoji[severity]} ${severity} (${severityResults.length})\n\n`;

for (const r of severityResults) {
const file = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri || '';
const line = r.locations?.[0]?.physicalLocation?.region?.startLine || '';
const location = file ? (line ? `${file}:${line}` : file) : 'Unknown location';

// Parse title and description from message
const msgParts = (r.message?.text || '').split(': ');
const title = msgParts[0] || r.ruleId;
const description = msgParts.slice(1).join(': ') || '';

body += `<details><summary><code>${r.ruleId}</code> - ${title}</summary>\n\n`;
body += `**Location:** \`${location}\`\n\n`;

if (description) {
body += `${description}\n\n`;
}

// Code snippet
const snippet = r.properties?.codeSnippet;
if (snippet) {
body += '```\n' + snippet + '\n```\n\n';
}

// CVEs and CWEs
const cves = r.properties?.cves || [];
const cwes = r.properties?.cwes || [];
if (cves.length > 0) {
body += `**CVEs:** ${cves.join(', ')}\n\n`;
}
if (cwes.length > 0) {
body += `**CWEs:** ${cwes.join(', ')}\n\n`;
}

// Package info
const pkg = r.properties?.package;
const version = r.properties?.version;
const fixVersion = r.properties?.fixVersion;
if (pkg) {
let pkgInfo = `**Package:** ${pkg}`;
if (version) pkgInfo += ` (${version})`;
if (fixVersion) pkgInfo += ` → Fix: ${fixVersion}`;
body += pkgInfo + '\n\n';
}

body += `</details>\n\n`;
}
}
}

body += `</details>`;
}

// Find and update existing comment, or create new
const { data: comments } = await github.rest.issues.listComments({
...context.repo,
issue_number: context.issue.number
});
const existing = comments.find(c => c.body.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
...context.repo,
comment_id: existing.id,
body
});
} else {
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body
});
}
# PR commenting now happens inside the Armis action (Post Branded PR Comment
# step), which sends raw SARIF to the backend for server-side branded
# formatting (posts as armis-appsec[bot]). See action.yml. The previous
# ~130-line github-script formatter was removed in PPSC-556.
Comment thread
yiftach-armis marked this conversation as resolved.

- name: Upload SARIF to GitHub Code Scanning
if: always()
Expand Down
Loading