Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 20 additions & 9 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84831,13 +84831,9 @@ function getRunContext() {
const isPullRequest = ctx.eventName === 'pull_request' || ctx.eventName === 'pull_request_target';
// payload.pull_request is loosely typed (index signature); read defensively.
const pr = ctx.payload.pull_request;
let isFork = false;
let prHeadRef;
if (isPullRequest && pr) {
prHeadRef = pr.head?.ref;
const headRepo = pr.head?.repo?.full_name;
const baseRepo = pr.base?.repo?.full_name;
isFork = Boolean(headRepo && baseRepo && headRepo !== baseRepo);
}
return {
eventName: ctx.eventName,
Expand All @@ -84848,7 +84844,6 @@ function getRunContext() {
prNumber: pr?.number,
prHeadRef,
isPullRequest,
isFork,
};
}

Expand Down Expand Up @@ -84909,10 +84904,18 @@ function evaluateGate(g) {
// unit-test without the Actions runtime. Replaces the bash branch/label logic
// from the v1 composite action (action.yml branch auto-detect + repo label).
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.isRemoteTarget = isRemoteTarget;
exports.repoLabel = repoLabel;
exports.refToBranch = refToBranch;
exports.resolveBranch = resolveBranch;
const GH_URL = /^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?\/?$/;
// isRemoteTarget reports whether the scan target is a remote URL rather than the
// local checkout. Caller-repo-scoped surfaces (Code Scanning SARIF upload, the PR
// comment) must be skipped for a remote target — their paths/commit/PR refer to
// THIS repo, not the scanned one, so they'd misattribute results.
function isRemoteTarget(target) {
return /^https?:\/\//i.test(target.trim());
}
// repoLabel resolves the human-facing repo label for the report. A GitHub URL
// target wins; otherwise the workflow's owner/repo; otherwise the raw target.
function repoLabel(target, ownerRepo) {
Expand Down Expand Up @@ -85355,8 +85358,12 @@ async function run() {
if (inputs.annotations) {
(0, annotations_1.emitAnnotations)(result.findings, inputs.maxAnnotations);
}
// A remote URL target scans a different repo than this checkout, so the
// caller-repo-scoped surfaces (Code Scanning upload, PR comment) would
// misattribute results to this repo — skip them.
const remoteTarget = (0, git_1.isRemoteTarget)(inputs.target);
// Surface 1b: SARIF → Security tab (needs security-events: write).
if (inputs.uploadSarif) {
if (inputs.uploadSarif && !remoteTarget) {
const res = await (0, sarif_1.uploadSarif)(inputs.githubToken, ctx, inputs.sarifFile);
core.setOutput('sarif-uploaded', String(res.uploaded));
if (res.uploaded)
Expand All @@ -85365,15 +85372,19 @@ async function run() {
core.warning(res.reason);
}
else {
if (inputs.uploadSarif && remoteTarget) {
core.info('Skipping Code Scanning upload: remote target — SARIF paths/commit do not match this repository.');
}
core.setOutput('sarif-uploaded', 'false');
}
// Downloadable artifact (JSON + SARIF).
if (inputs.uploadArtifact) {
const days = inputs.artifactRetentionDays ? parseInt(inputs.artifactRetentionDays, 10) : undefined;
await (0, artifact_1.uploadResults)(inputs.artifactName, [inputs.jsonFile, inputs.sarifFile], days);
}
// Surface 2: sticky PR comment (needs pull-requests: write; pull_request only).
if (inputs.commentOnPr && ctx.isPullRequest) {
// Surface 2: sticky PR comment (needs pull-requests: write; pull_request only;
// skipped for a remote target — the comment would describe a different repo).
if (inputs.commentOnPr && ctx.isPullRequest && !remoteTarget) {
await (0, comment_1.upsertComment)(inputs.githubToken, ctx, md);
}
// Surface 3: status-check gating — the job status is the check.
Expand Down Expand Up @@ -85591,7 +85602,7 @@ function buildConsoleLines(d) {
L.push(row(`Max severity: ${d.maxSeverity} Native exit: ${d.nativeExit}`));
L.push(rule());
if (d.projected) {
L.push("Projected = estimate from trustabl's own scoring; listed fixes resolved, nothing new. Not a re-scan.");
L.push('Projected = estimate (listed fixes resolved), not a re-scan.');
}
return L;
}
Expand Down
10 changes: 1 addition & 9 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,18 @@ export interface RunContext {
prNumber?: number;
prHeadRef?: string;
isPullRequest: boolean;
isFork: boolean;
}

export function getRunContext(): RunContext {
const ctx = github.context;
const isPullRequest =
ctx.eventName === 'pull_request' || ctx.eventName === 'pull_request_target';
// payload.pull_request is loosely typed (index signature); read defensively.
const pr = ctx.payload.pull_request as
| { number?: number; head?: any; base?: any }
| undefined;
const pr = ctx.payload.pull_request as { number?: number; head?: any } | undefined;

let isFork = false;
let prHeadRef: string | undefined;
if (isPullRequest && pr) {
prHeadRef = pr.head?.ref;
const headRepo: string | undefined = pr.head?.repo?.full_name;
const baseRepo: string | undefined = pr.base?.repo?.full_name;
isFork = Boolean(headRepo && baseRepo && headRepo !== baseRepo);
}

return {
Expand All @@ -45,6 +38,5 @@ export function getRunContext(): RunContext {
prNumber: pr?.number,
prHeadRef,
isPullRequest,
isFork,
};
}
16 changes: 15 additions & 1 deletion src/git.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
import { repoLabel, refToBranch, resolveBranch } from './git';
import { repoLabel, refToBranch, resolveBranch, isRemoteTarget } from './git';

describe('isRemoteTarget', () => {
it('detects http(s) URLs (trimmed)', () => {
expect(isRemoteTarget('https://github.com/o/r')).toBe(true);
expect(isRemoteTarget('http://example.com/x')).toBe(true);
expect(isRemoteTarget(' https://github.com/o/r ')).toBe(true);
});
it('treats local paths and empty as not remote', () => {
expect(isRemoteTarget('.')).toBe(false);
expect(isRemoteTarget('./sub/dir')).toBe(false);
expect(isRemoteTarget('/abs/path')).toBe(false);
expect(isRemoteTarget('')).toBe(false);
});
});

describe('repoLabel', () => {
it('extracts owner/repo from a GitHub URL, stripping .git and trailing slash', () => {
Expand Down
8 changes: 8 additions & 0 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

const GH_URL = /^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?\/?$/;

// isRemoteTarget reports whether the scan target is a remote URL rather than the
// local checkout. Caller-repo-scoped surfaces (Code Scanning SARIF upload, the PR
// comment) must be skipped for a remote target — their paths/commit/PR refer to
// THIS repo, not the scanned one, so they'd misattribute results.
export function isRemoteTarget(target: string): boolean {
return /^https?:\/\//i.test(target.trim());
}

// repoLabel resolves the human-facing repo label for the report. A GitHub URL
// target wins; otherwise the workflow's owner/repo; otherwise the raw target.
export function repoLabel(target: string, ownerRepo: string): string {
Expand Down
19 changes: 15 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { resolveTrustabl } from './install';
import { runScan } from './runner';
import { readiness, risk, maxSeverity, severityCounts, projectedReadiness } from './score';
import { evaluateGate } from './gate';
import { repoLabel, resolveBranch } from './git';
import { repoLabel, resolveBranch, isRemoteTarget } from './git';
import { ReportData } from './report/model';
import { renderConsole } from './report/console';
import { buildSummaryMarkdown, writeStepSummary } from './report/summary';
Expand Down Expand Up @@ -90,13 +90,23 @@ async function run(): Promise<void> {
emitAnnotations(result.findings, inputs.maxAnnotations);
}

// A remote URL target scans a different repo than this checkout, so the
// caller-repo-scoped surfaces (Code Scanning upload, PR comment) would
// misattribute results to this repo — skip them.
const remoteTarget = isRemoteTarget(inputs.target);

// Surface 1b: SARIF → Security tab (needs security-events: write).
if (inputs.uploadSarif) {
if (inputs.uploadSarif && !remoteTarget) {
const res = await uploadSarif(inputs.githubToken, ctx, inputs.sarifFile);
core.setOutput('sarif-uploaded', String(res.uploaded));
if (res.uploaded) core.info('Uploaded SARIF to Code Scanning.');
else if (res.reason) core.warning(res.reason);
} else {
if (inputs.uploadSarif && remoteTarget) {
core.info(
'Skipping Code Scanning upload: remote target — SARIF paths/commit do not match this repository.',
);
}
core.setOutput('sarif-uploaded', 'false');
}

Expand All @@ -106,8 +116,9 @@ async function run(): Promise<void> {
await uploadResults(inputs.artifactName, [inputs.jsonFile, inputs.sarifFile], days);
}

// Surface 2: sticky PR comment (needs pull-requests: write; pull_request only).
if (inputs.commentOnPr && ctx.isPullRequest) {
// Surface 2: sticky PR comment (needs pull-requests: write; pull_request only;
// skipped for a remote target — the comment would describe a different repo).
if (inputs.commentOnPr && ctx.isPullRequest && !remoteTarget) {
await upsertComment(inputs.githubToken, ctx, md);
}

Expand Down
2 changes: 1 addition & 1 deletion src/report/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function buildConsoleLines(d: ReportData): string[] {
L.push(row(`Max severity: ${d.maxSeverity} Native exit: ${d.nativeExit}`));
L.push(rule());
if (d.projected) {
L.push("Projected = estimate from trustabl's own scoring; listed fixes resolved, nothing new. Not a re-scan.");
L.push('Projected = estimate (listed fixes resolved), not a re-scan.');
}
return L;
}
Expand Down
Loading