From c23f67302d8c8c66dc6cbb3cc5a9eb2d1c6cc96f Mon Sep 17 00:00:00 2001 From: Conal <33135619+Conalh@users.noreply.github.com> Date: Fri, 22 May 2026 16:05:20 -0700 Subject: [PATCH] Tighten CLI parity, monorepo annotations, and workflow noise - Add --fail-on threshold to the CLI matching the Action behavior so local pre-commit and non-GHA CI runs can fail on findings instead of always exiting 0. - Strip the /src/, /.github/workflows/, /package.json lastIndexOf heuristic from normalizeGitDiffPath. Git already gives repo-relative paths; the heuristic mangled monorepo paths (apps/web/src/foo.ts -> src/foo.ts), misplacing GitHub annotations. - URL-gate detectExternalCurl so curl/wget/Invoke-WebRequest/fetch lines only flag when an external https?:// URL or a variable substitution is present. Excludes localhost/127.0.0.1/0.0.0.0/::1 and benign mentions. detectSecretExfil is untouched. - Share severityRank between report.ts and action.ts; CLI uses the same export. - Align the README demo bullet with the actual fixture file (src/api/sync.ts). Adds 9 tests (73 total, all passing). Co-Authored-By: Claude Opus 4.7 --- README.md | 2 +- dist/action-bundle/index.js | 2 +- dist/action.js | 9 +-- dist/detectors/workflow-permissions.js | 18 ++++++ dist/git-diff.js | 16 ++--- dist/index.js | 26 ++++++-- dist/report.js | 2 +- src/action.ts | 10 +--- src/detectors/workflow-permissions.ts | 23 ++++++++ src/git-diff.ts | 18 ++---- src/index.ts | 34 ++++++++--- src/report.ts | 2 +- test/cli-output.test.mjs | 82 ++++++++++++++++++++++++++ test/detectors.test.mjs | 59 ++++++++++++++++++ test/git-diff.test.mjs | 39 ++++++++++++ 15 files changed, 283 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 4dae542..aa44f91 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Live demo PR: [Demo: code-only capability drift](https://github.com/Conalh/Capab That PR intentionally adds only application and workflow changes: -- A new `src/telemetry/client.ts` file with an external `fetch()` call. +- A new `src/api/sync.ts` file with an external `fetch()` call. - A `postinstall` script that pipes a remote installer into `bash`. - GitHub Actions `contents: write` permission and a `curl` bootstrap step. diff --git a/dist/action-bundle/index.js b/dist/action-bundle/index.js index 1b79740..b2b819c 100644 --- a/dist/action-bundle/index.js +++ b/dist/action-bundle/index.js @@ -1 +1 @@ -import{createRequire as e}from"module";var t={929:(e,t,n)=>{n.a(e,(async(e,i)=>{try{n.d(t,{g:()=>mainAction});var s=n(455);var r=n.n(s);var o=n(499);var a=n(924);var c=n(665);const l={none:0,low:1,medium:2,high:3,critical:4};async function mainAction(e=process.env){const t=getInput(e,"repo")||e.GITHUB_WORKSPACE||process.cwd();const n=await readEvent(e);const i=getInput(e,"base")||getDefaultBase(e,n);const s=getInput(e,"head")||getDefaultHead(e,n);const r=getInput(e,"fail-on")||"none";const u=r.toLowerCase();if(!i||!s){writeError("CapabilityEcho needs base and head refs. Pass base/head inputs or run on pull_request with actions/checkout fetch-depth: 0.");return 2}if(!isRating(u)){writeError(`Invalid fail-on value '${r}'. Use none, low, medium, high, or critical.`);return 2}let f;try{f=await(0,o.X)({mode:"git",repo:t,base:i,head:s})}catch(e){if(e instanceof a.rd){writeError(`CapabilityEcho could not compare base '${e.base}' and head '${e.head}'. Ensure actions/checkout uses fetch-depth: 0, or pass refs that exist in the checkout through the \`base\` and \`head\` inputs.`);return 2}throw e}const p=(0,c.B)(f,"markdown");const d=(0,c.B)(f,"json");const h=JSON.stringify({rating:f.rating,hasFindings:f.findingCount>0,findingCount:f.findingCount,changedFileCount:f.changedFileCount,surfaceSummary:f.surfaceSummary,severitySummary:f.severitySummary,capabilitySummary:f.capabilitySummary,topRecommendations:f.topRecommendations});process.stdout.write(p);process.stdout.write((0,c.B)(f,"github"));await appendIfSet(e.GITHUB_STEP_SUMMARY,p);await writeOutput(e,"rating",f.rating);await writeOutput(e,"has-findings",String(f.findingCount>0));await writeOutput(e,"finding-count",String(f.findingCount));await writeOutput(e,"changed-file-count",String(f.changedFileCount));await writeOutput(e,"surface-summary",JSON.stringify(f.surfaceSummary));await writeOutput(e,"severity-summary",JSON.stringify(f.severitySummary));await writeOutput(e,"capability-summary",JSON.stringify(f.capabilitySummary));await writeOutput(e,"top-recommendations",JSON.stringify(f.topRecommendations));await writeOutput(e,"adoption-evidence",h);await writeOutput(e,"report-markdown",p);await writeOutput(e,"report-json",d);if(l[u]>0&&l[f.rating]>=l[u]){writeError(`CapabilityEcho capability drift rating ${f.rating} meets fail-on threshold ${u}.`);return 1}return 0}function getInput(e,t){const n=e[`INPUT_${t.replace(/ /g,"_").toUpperCase()}`];const i=e[`INPUT_${t.replace(/[- ]/g,"_").toUpperCase()}`];return(n||i||"").trim()}async function readEvent(e){if(!e.GITHUB_EVENT_PATH){return{}}try{const t=await(0,s.readFile)(e.GITHUB_EVENT_PATH,"utf8");const n=JSON.parse(t);return isRecord(n)?n:{}}catch{return{}}}function getDefaultBase(e,t){const n=t.pull_request;if(isRecord(n)&&isRecord(n.base)&&typeof n.base.sha==="string"){return n.base.sha}if(typeof t.before==="string"){return t.before}return e.DEFAULT_BASE||""}function getDefaultHead(e,t){const n=t.pull_request;if(isRecord(n)&&isRecord(n.head)&&typeof n.head.sha==="string"){return n.head.sha}if(typeof t.after==="string"){return t.after}return e.DEFAULT_HEAD||e.GITHUB_SHA||""}async function writeOutput(e,t,n){if(!e.GITHUB_OUTPUT){return}if(n.includes("\n")||n.includes("\r")){const i=outputDelimiter(t,n);const r=n.endsWith("\n")?n:`${n}\n`;await(0,s.appendFile)(e.GITHUB_OUTPUT,`${t}<<${i}\n${r}${i}\n`,"utf8");return}await(0,s.appendFile)(e.GITHUB_OUTPUT,`${t}=${n}\n`,"utf8")}function outputDelimiter(e,t){const n=e.replace(/[^A-Za-z0-9_]+/g,"_");let i=`capabilityecho_${n}_EOF`;let s=1;while(t.includes(i)){i=`capabilityecho_${n}_EOF_${s}`;s+=1}return i}async function appendIfSet(e,t){if(!e){return}await(0,s.appendFile)(e,t,"utf8")}function writeError(e){process.stdout.write(`::error::${escapeMessage(e)}\n`)}function escapeMessage(e){return e.replaceAll("%","%25").replaceAll("\r","%0D").replaceAll("\n","%0A")}function isRating(e){return e==="none"||e==="low"||e==="medium"||e==="high"||e==="critical"}function isRecord(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}if(process.argv[1]?.endsWith("action.js")){process.exitCode=await mainAction()}i()}catch(u){i(u)}}),1)},499:(t,n,i)=>{i.d(n,{X:()=>runCapabilityDiff});var s=i(431);function detectDockerfileCapability(e){const t=[];for(const n of e){if(!(0,s.pZ)(n.file)||(0,s.w5)(n.content)){continue}t.push(...detectRemoteAdd(n));t.push(...detectPipeToShell(n))}return t}function detectRemoteAdd(e){if(!/^\s*ADD\s+https?:\/\//i.test(e.content)){return[]}return[{kind:"capability_echo.dockerfile_remote_add",surface:"container",severity:"high",file:e.file,line:e.line,subject:"Dockerfile remote ADD",message:"Dockerfile adds remote content during image build, expanding build-time network reach.",recommendation:"Download pinned artifacts with checksum verification, or vendor reviewed files into the repository."}]}function detectPipeToShell(e){if(!/^\s*RUN\b.*(?:curl|wget)[^\n|]*https?:\/\/[^\n|]*\|\s*(?:ba)?sh\b/i.test(e.content)){return[]}return[{kind:"capability_echo.dockerfile_pipe_to_shell",surface:"container",severity:"critical",file:e.file,line:e.line,subject:"Dockerfile pipe-to-shell",message:"Dockerfile downloads remote content and pipes it directly to a shell during image build.",recommendation:"Replace remote pipe-to-shell with pinned, reviewable build steps and checksum verification."}]}function detectJsCapability(e,t={}){const n=[];const i=collectSecretVariables(e,t);for(const t of e){if(!(0,s.Mn)(t.file)||(0,s.w5)(t.content)){continue}const e=(0,s.hl)(t.file);n.push(...detectFetch(t,e));n.push(...detectSecretExfil(t,e,i.get(t.file)??new Set));n.push(...detectSubprocess(t,e));n.push(...detectDynamicEval(t,e))}return n}function collectSecretVariables(e,t){const n=new Map;for(const t of e){if(!(0,s.Mn)(t.file)){continue}addSecretVariable(n,t.file,t.content)}for(const[e,i]of Object.entries(t)){if(!(0,s.Mn)(e)){continue}for(const t of i.split(/\r?\n/)){addSecretVariable(n,e,t)}}return n}function addSecretVariable(e,t,n){const i=n.match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*process\.env\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b/i);if(!i){return}const s=e.get(t)??new Set;s.add(i[1]);e.set(t,s)}function detectFetch(e,t){if(!/(?:fetch\s*\(|axios\.(?:get|post|put|delete|patch|request)\s*\(|got\s*\()/i.test(e.content)){return[]}if(!/(?:https?:\/\/|['"]https?:\/\/)/i.test(e.content)){return[]}if(/(?:fetch\s*\(\s*['"`]\/|axios\.(?:get|post|put|delete|patch|request)\s*\(\s*['"`]\/)/i.test(e.content)){return[]}return[{kind:"capability_echo.external_fetch_added",surface:"source",severity:t?"low":"medium",file:e.file,line:e.line,subject:"External network fetch",message:"Added code performs an external HTTP request that expands network reach.",recommendation:"Review the endpoint, data sent, and whether the request belongs in this change."}]}function detectSecretExfil(e,t,n){if(!isExternalHttpRequest(e.content)||!referencesEnvSecret(e.content)&&!referencesSecretVariable(e.content,n)){return[]}return[{kind:"capability_echo.source_secret_exfil_pattern",surface:"source",severity:t?"medium":"high",file:e.file,line:e.line,subject:"Source secret exfiltration pattern",message:"Added source code sends environment-secret-shaped data to an external endpoint.",recommendation:"Do not send env secrets to external services unless the endpoint and payload are explicitly required."}]}function isExternalHttpRequest(e){return/(?:fetch\s*\(|axios\.(?:get|post|put|delete|patch|request)\s*\(|got\s*\()/i.test(e)&&/(?:https?:\/\/|['"]https?:\/\/)/i.test(e)}function referencesEnvSecret(e){return/\bprocess\.env\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b/i.test(e)}function referencesSecretVariable(e,t){return[...t].some((t=>new RegExp(String.raw`\b${escapeRegExp(t)}\b`).test(e)))}function escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function detectSubprocess(e,t){if(!/(?:child_process|execSync\s*\(|exec\s*\(|spawnSync\s*\(|spawn\s*\(|Bun\.spawn\s*\()/i.test(e.content)){return[]}return[{kind:"capability_echo.subprocess_spawn_added",surface:"source",severity:t?"low":"high",file:e.file,line:e.line,subject:"Subprocess spawn",message:"Added code can spawn shell commands or subprocesses.",recommendation:"Confirm the command source is trusted and scoped to the task."}]}function detectDynamicEval(e,t){if(!/(?:\beval\s*\(|new\s+Function\s*\(|vm\.runInNewContext\s*\()/i.test(e.content)){return[]}return[{kind:"capability_echo.dynamic_eval_added",surface:"source",severity:t?"medium":"critical",file:e.file,line:e.line,subject:"Dynamic code execution",message:"Added code can evaluate dynamic JavaScript at runtime.",recommendation:"Avoid eval-style execution unless strictly required and heavily constrained."}]}var r=i(455);var o=i(760);const a=e(import.meta.url)("node:crypto");const c=null&&["low","medium","high","critical"];const l=null&&["scope_trail","policy_mesh","capability_echo","task_bound","session_trail"];function isSeverity(e){return typeof e==="string"&&c.includes(e)}function isToolKind(e){return typeof e==="string"&&l.includes(e)}function kind(e,t){if(!/^[a-z0-9_]+$/.test(t)){throw new Error(`agent-gov-core/kind: name '${t}' must match [a-z0-9_]+ (kebab, camelCase, and dots are rejected)`)}return`${e}.${t}`}const u=/^(scope_trail|policy_mesh|capability_echo|task_bound|session_trail)\.[a-z0-9_]+$/;function isNamespacedKind(e){return typeof e==="string"&&u.test(e)}function createFinding(e){const t={tool:e.tool,kind:kind(e.tool,e.name),severity:e.severity,message:e.message};if(e.detail!==undefined)t.detail=e.detail;if(e.location!==undefined)t.location=e.location;if(e.salientKey!==undefined)t.salientKey=e.salientKey;if(e.data!==undefined)t.data=e.data;t.fingerprint=e.fingerprint??fingerprintFinding(t);return t}function fingerprintFinding(e){const t=e.location?.file?.replace(/\\/g,"/")??"";const n=[e.kind,t,e.location?.line??"",e.location?.column??""];if(e.salientKey!==undefined){n.push(e.salientKey)}return createHash("sha256").update(n.join("|")).digest("hex").slice(0,16)}const f=new Set(["tool","kind","severity","message","detail","location","fingerprint","salientKey","data"]);const p=new Set(["file","line","column","endLine","endColumn"]);function validateFinding(e){const t=[];if(e===null||typeof e!=="object"||Array.isArray(e)){return{ok:false,errors:["finding must be a plain object"]}}const n=e;if(!isToolKind(n.tool))t.push(`tool must be one of: ${l.join(", ")}`);if(!isNamespacedKind(n.kind))t.push("kind must match '.' (e.g. 'scope_trail.permission_allow_widened')");if(!isSeverity(n.severity))t.push(`severity must be one of: ${c.join(", ")}`);if(typeof n.message!=="string"||n.message.length===0)t.push("message must be a non-empty string");if(isToolKind(n.tool)&&isNamespacedKind(n.kind)&&!n.kind.startsWith(`${n.tool}.`)){t.push(`kind '${n.kind}' must start with tool '${n.tool}.'`)}if(n.detail!==undefined&&typeof n.detail!=="string"){t.push("detail must be a string when present")}if(n.fingerprint!==undefined&&typeof n.fingerprint!=="string"){t.push("fingerprint must be a string when present")}if(n.salientKey!==undefined&&typeof n.salientKey!=="string"){t.push("salientKey must be a string when present")}if(n.data!==undefined&&(n.data===null||typeof n.data!=="object"||Array.isArray(n.data))){t.push("data must be an object when present")}if(n.location!==undefined){t.push(...validateLocation(n.location))}for(const e of Object.keys(n)){if(!f.has(e))t.push(`unknown property: ${e}`)}return{ok:t.length===0,errors:t}}function validateLocation(e){const t=[];if(e===null||typeof e!=="object"||Array.isArray(e)){return["location must be an object when present"]}const n=e;if(typeof n.file!=="string"||n.file.length===0){t.push("location.file must be a non-empty string")}for(const e of["line","column","endLine","endColumn"]){if(n[e]!==undefined){const i=n[e];if(typeof i!=="number"||!Number.isInteger(i)||i<1){t.push(`location.${e} must be a positive integer when present`)}}}for(const e of Object.keys(n)){if(!p.has(e))t.push(`unknown location property: ${e}`)}return t}const d=e(import.meta.url)("node:fs");class ConfigParseError extends Error{line;column;rawOffset;constructor(e,t){super(e);this.name="ConfigParseError";this.line=t.line;this.column=t.column;this.rawOffset=t.rawOffset;if(t.cause){this.cause=t.cause}}}function lineColumnOfOffset(e,t){const n=Math.max(0,Math.min(t,e.length));let i=1;let s=1;for(let t=0;t=this.len)break;const e=this.src[this.pos];if(e==="#"){this.skipComment();continue}if(e==="["){if(this.src[this.pos+1]==="["){this.parseArrayOfTablesHeader()}else{this.parseTableHeader()}continue}this.parseKeyValue(this.current)}return this.root}skipWhitespaceAndNewlines(){while(this.pos=this.len)return;const e=this.src[this.pos];if(e==="#"){this.skipComment();return}if(e==="\n"||e==="\r")return;throw new Error(`Unexpected character ${JSON.stringify(e)} at offset ${this.pos}; expected end of line`)}parseTableHeader(){this.pos++;this.skipInlineWhitespace();const e=this.parseKeyChain();this.skipInlineWhitespace();if(this.src[this.pos]!=="]"){throw new Error(`Expected ']' at offset ${this.pos}`)}this.pos++;this.expectLineEnd();const t=e.join(this.PATH_KEY_SEPARATOR);if(this.aotPaths.has(t)){throw new Error(`Cannot redefine array-of-tables [[${e.join(".")}]] as a standard table [${e.join(".")}] at offset ${this.pos}`)}const n=this.descendTablePath(e,true);if(this.definedTables.has(t)){throw new Error(`Duplicate table definition: [${e.join(".")}] at offset ${this.pos}`)}this.definedTables.add(t);this.current=n}parseArrayOfTablesHeader(){this.pos+=2;this.skipInlineWhitespace();const e=this.parseKeyChain();this.skipInlineWhitespace();if(this.src[this.pos]!=="]"||this.src[this.pos+1]!=="]"){throw new Error(`Expected ']]' at offset ${this.pos}`)}this.pos+=2;this.expectLineEnd();const t=this.descendTablePath(e.slice(0,-1),true);const n=e[e.length-1];let i=t[n];if(i===undefined){i=[];t[n]=i;this.aotPaths.add(e.join(this.PATH_KEY_SEPARATOR))}else if(!Array.isArray(i)){throw new Error(`Key ${e.join(".")} is not an array-of-tables`)}const s=e.join(this.PATH_KEY_SEPARATOR)+this.PATH_KEY_SEPARATOR;for(const e of this.definedTables){if(e.startsWith(s)){this.definedTables.delete(e)}}const r={};i.push(r);this.current=r}descendTablePath(e,t){let n=this.root;for(let t=0;te===i[t]));if(!f)continue;const p=i.slice(o.length);if(p.length===0)continue;const d=p.map(escapeForRegex).join("\\s*\\.\\s*");const h=new RegExp(`^\\s*${d}\\s*=`);if(h.test(n))return t;if(p.length===1){const e=p[0];const i=new RegExp(`^\\s*(?:${escapeForRegex(e)}|"${escapeForRegex(e)}"|'${escapeForRegex(e)}')\\s*(?:\\.|=)`);if(i.test(n))return t}}return 0}function updateMultilineStringState(e,t){let n=t;let i=0;while(itrue;const n=lineOfOffset(e,t.start);const i=lineOfOffset(e,Math.max(t.start,t.end-1));return e=>e>=n&&e<=i}function findLineByRegex(e,t,n){const i=stripJsonComments(e);const s=n?i.slice(n.start,n.end):i;const r=t.exec(s);if(!r)return 0;const o=(n?n.start:0)+r.index;return lineOfOffset(e,o)}function lineOfOffset(e,t){let n=1;for(let i=0;i=i)break;const s=e[n];if(s==='"'){n++;const s=n;while(n`${e}=${t}`)).sort();t.push(`env=${n.join("|")}`)}return t.join("\n")}function normalizeExecutable(e){const t=e.trim();const n=t.replace(/\\/g,"/");const i=/\.(cmd|exe|bat|ps1)$/i.test(n);const s=n.replace(/\.(cmd|exe|bat|ps1)$/i,"");const r=i||t.includes("\\");const o=r?s.toLowerCase():s;const a=o.split("/").pop()??o;if(g.has(a.toLowerCase())){return r?a.toLowerCase():a}return o}const g=new Set(["node","npx","npm","pnpm","yarn","python","python3","pip","pip3","pipx","uvx","uv","ruby","gem","bundle","perl","cpan","bash","sh","zsh","fish","powershell","pwsh","deno","bun","tsx","ts-node"]);function normalizePath(e){return e.trim().replace(/\\/g,"/").replace(/\/+$/,"")}const w=new Set(["-y","--yes"]);const y=new Set(["-v","-V","-q","-h","-d","--verbose","--quiet","--silent","--debug","--help","--version","--force","--dry-run","--no-cache","--no-color","--no-progress","--json"]);function canonicalizeArgs(e){const t=e.filter((e=>!w.has(e)));const n=[];const i=[];let s=false;let r=0;for(let e=0;eet?1:0));const o=[...n];for(const[e,t]of i){if(e.startsWith("__pos_")){o.push(e.slice(e.indexOf("__",6)+2))}else if(t===null){o.push(e)}else{o.push(`${e}=${t}`)}}return o}const b={low:1,medium:2,high:3,critical:4};function rankSeverity(e){return b[e]}function passesSeverityThreshold(e,t){return rankSeverity(e)>=rankSeverity(t)}function anyAtOrAbove(e,t){for(const n of e){if(passesSeverityThreshold(n.severity,t))return true}return false}function emitFindingAnnotation(e){const t=e.severity==="critical"||e.severity==="high"?"error":"warning";const n=[];if(e.location?.file)n.push(`file=${escapeProperty(e.location.file)}`);if(e.location?.line!=null)n.push(`line=${e.location.line}`);if(e.location?.column!=null)n.push(`col=${e.location.column}`);if(e.location?.endLine!=null)n.push(`endLine=${e.location.endLine}`);if(e.location?.endColumn!=null)n.push(`endColumn=${e.location.endColumn}`);n.push(`title=${escapeProperty(`[${e.kind}] ${e.severity}`)}`);const i=escapeData(e.message);return`::${t} ${n.join(",")}::${i}`}function escapeData(e){return e.replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function escapeProperty(e){return e.replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A").replace(/:/g,"%3A").replace(/,/g,"%2C")}function generateWorkflowSummary(e,t={}){const n=t.title??"Findings";const i=t.perSeverityLimit??100;const s=t.messageMaxLength??200;if(e.length===0){return`# ${n}\n\nNo findings.\n`}const r={critical:[],high:[],medium:[],low:[]};for(const t of e)r[t.severity].push(t);const o={critical:r.critical.length,high:r.high.length,medium:r.medium.length,low:r.low.length};const a=[];a.push(`# ${n}`,"");a.push(`**Total**: ${e.length} finding${e.length===1?"":"s"} — `+`${o.critical} critical, ${o.high} high, `+`${o.medium} medium, ${o.low} low`);a.push("");const c=["critical","high","medium","low"];for(const e of c){const t=r[e];if(t.length===0)continue;const n=t.slice(0,i);const o=t.length-n.length;a.push(``);a.push(`${t.length} ${e}`);a.push("");a.push("| File | Line | Kind | Message |");a.push("|------|------|------|---------|");for(const e of n){a.push("| "+[escapeMarkdownTableCell(e.location?.file??"—"),e.location?.line??"—",escapeMarkdownTableCell(e.kind),escapeMarkdownTableCell(truncate(e.message,s))].join(" | ")+" |")}if(o>0){a.push(`| _(+${o} more ${e} finding${o===1?"":"s"})_ | | | |`)}a.push("");a.push("");a.push("")}return a.join("\n")}function truncate(e,t){if(e.length<=t)return e;return e.slice(0,Math.max(1,t-1))+"…"}function escapeMarkdownTableCell(e){return String(e).replace(/&/g,"&").replace(//g,">").replace(/\|/g,"\\|").replace(/\r?\n/g," ")}async function readJsonObject(e){return(await discovery_readJsonObjectWithSource(e)).json}async function discovery_readJsonObjectWithSource(e){try{const t=await readFile(e,"utf8");const n=JSON.parse(t);return{json:isRecord(n)?n:{},text:t}}catch(e){if(isNodeError(e)&&e.code==="ENOENT"){return{json:{},text:""}}throw e}}function configPath(e,t){return(0,o.join)(e,t)}function isRecord(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function discovery_lineOfJsonKey(e,t){const n=lineOfJsonKey(e,t);return n===0?undefined:n}function discovery_lineOfJsonStringValue(e,t){const n=lineOfJsonStringValue(e,t);return n===0?undefined:n}function isNodeError(e){return e instanceof Error&&"code"in e}var _=i(924);const k=["postinstall","preinstall","prepare","install"];async function detectPackageScripts(e){const t=e.mode==="directories"?await(0,_.kf)(e.newRoot):await listChangedPackageJsonFiles(e.repo,e.base,e.head);const n=[];for(const i of t){const t=await readScriptsAt(e,i,"old");const s=await readScriptsAt(e,i,"new");const r=await readPackageTextAt(e,i,"new");n.push(...compareScripts(i,t,s,r))}return n}async function listChangedPackageJsonFiles(e,t,n){return(await(0,_.Eh)(e,t,n)).filter(s.xr)}async function readScriptsAt(e,t,n){const i=await readPackageTextAt(e,t,n);if(!i){return{}}try{const e=JSON.parse(i);if(!isRecord(e)||!isRecord(e.scripts)){return{}}const t={};for(const[n,i]of Object.entries(e.scripts)){if(typeof i==="string"){t[n]=i}}return t}catch{return{}}}async function readPackageTextAt(e,t,n){if(e.mode==="directories"){const i=n==="old"?e.oldRoot:e.newRoot;try{return await(0,r.readFile)(configPath(i,t),"utf8")}catch{return""}}const i=n==="old"?e.base:e.head;return await(0,_.p9)(e.repo,i,t)??""}function compareScripts(e,t,n,i){const s=[];for(const r of k){const o=n[r];if(!o){continue}const a=t[r];if(a===o){continue}const c=discovery_lineOfJsonKey(i,r)??discovery_lineOfJsonStringValue(i,o);s.push({kind:"capability_echo.lifecycle_script_added",surface:"package",severity:"high",file:e,line:c,subject:`package.json ${r} script`,message:`Added or changed npm ${r} lifecycle script.`,recommendation:"Review lifecycle scripts carefully; they run automatically on install."});s.push(...analyzeScriptContent(e,r,o,i))}for(const[r,o]of Object.entries(n)){if(k.includes(r)){continue}const n=t[r];if(n===o){continue}s.push(...analyzeScriptContent(e,r,o,i))}return s}function analyzeScriptContent(e,t,n,i){const s=[];const r=discovery_lineOfJsonStringValue(i,n)??discovery_lineOfJsonKey(i,t);if(/(?:curl[^\n|]*\|\s*(?:ba)?sh|wget[^\n|]*\|\s*sh|Invoke-Expression|iex\s*\()/i.test(n)){s.push({kind:"capability_echo.script_pipe_to_shell",surface:"package",severity:"critical",file:e,line:r,subject:`package.json ${t} pipe-to-shell`,message:"Script downloads and pipes content directly into a shell.",recommendation:"Replace remote pipe-to-shell patterns with pinned, reviewable install steps."})}if(/\b(curl|wget|npm publish)\b/i.test(n)||/\bnpx\b(?![^\s]*@\d+\.\d+\.\d+)/i.test(n)){s.push({kind:"capability_echo.script_network_command",surface:"package",severity:"medium",file:e,line:r,subject:`package.json ${t} network command`,message:"Script performs a network or publish command.",recommendation:"Pin package versions and verify remote commands before merge."})}return s}const v=new Set(["puppeteer","puppeteer-core","playwright","playwright-core","cypress","webdriverio","selenium-webdriver","nightwatch","execa","cross-spawn","node-pty","shelljs","zx","tinyspawn","node-fetch","undici","got","axios","request","superagent","vm2","isolated-vm","socks-proxy-agent","https-proxy-agent","ssh2","node-ssh"]);const S=new Set(["@segment/analytics-node","mixpanel","amplitude-js","posthog-js","@sentry/node","@sentry/browser"]);const E=["dependencies","devDependencies","optionalDependencies","peerDependencies"];async function detectPackageDeps(e){const t=e.mode==="directories"?await(0,_.kf)(e.newRoot):await listChangedPackageJsonFiles(e.repo,e.base,e.head);const n=[];for(const i of t){const t=await readPackageTextAt(e,i,"old");const s=await readPackageTextAt(e,i,"new");n.push(...compareDeps(i,t,s))}return n}function compareDeps(e,t,n){const i=readAllDeps(t);const s=readAllDeps(n);const r=[];for(const[t,o]of s.entries()){if(i.has(t)){continue}if(v.has(t)){r.push({kind:"capability_echo.high_capability_dep_added",surface:"package",severity:"high",file:e,line:discovery_lineOfJsonStringValue(n,o)??discovery_lineOfJsonKey(n,t),subject:t,message:`Added dependency "${t}" can reach the network, spawn subprocesses, or evaluate code.`,recommendation:"Confirm this dependency is required for the stated change and that its usage is scoped."});continue}if(S.has(t)){r.push({kind:"capability_echo.telemetry_dep_added",surface:"package",severity:"medium",file:e,line:discovery_lineOfJsonStringValue(n,o)??discovery_lineOfJsonKey(n,t),subject:t,message:`Added telemetry/analytics dependency "${t}" — ships an outbound network surface by default.`,recommendation:"Verify the telemetry destination, payload, and opt-out posture."})}}return r}function readAllDeps(e){const t=new Map;if(!e.trim()){return t}let n;try{n=JSON.parse(e)}catch{return t}if(!isRecord(n)){return t}for(const e of E){const i=n[e];if(!isRecord(i)){continue}for(const[e,n]of Object.entries(i)){if(typeof n==="string"){t.set(e,n)}}}return t}function detectPyCapability(e,t={}){const n=[];const i=py_capability_collectSecretVariables(e,t);for(const t of e){if(!(0,s.UD)(t.file)||(0,s.w5)(t.content)){continue}const e=(0,s.hl)(t.file);n.push(...detectPyNetwork(t,e));n.push(...detectPySecretExfil(t,e,i.get(t.file)??new Set));n.push(...detectPySubprocess(t,e));n.push(...detectPyDynamicExec(t,e));n.push(...detectPyUnsafeDeserialize(t,e))}return n}function py_capability_collectSecretVariables(e,t){const n=new Map;for(const t of e){if(!(0,s.UD)(t.file)){continue}py_capability_addSecretVariable(n,t.file,t.content)}for(const[e,i]of Object.entries(t)){if(!(0,s.UD)(e)){continue}for(const t of i.split(/\r?\n/)){py_capability_addSecretVariable(n,e,t)}}return n}function py_capability_addSecretVariable(e,t,n){const i=n.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:os\.environ\s*(?:\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\]|\.get\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"])|os\.getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"])/i);if(!i){return}const s=e.get(t)??new Set;s.add(i[1]);e.set(t,s)}function detectPyNetwork(e,t){const n=/\b(?:requests|httpx)\.(?:get|post|put|delete|patch|head|options|request)\s*\(|\burllib(?:2)?\.(?:request\.)?urlopen\s*\(|\burlopen\s*\(|\burllib\.request\.urlretrieve\s*\(|\baiohttp\.ClientSession\s*\(/i;if(!n.test(e.content)){return[]}if(!/(?:https?:\/\/|['"]https?:\/\/)/i.test(e.content)){return[]}return[{kind:"capability_echo.external_fetch_added",surface:"source",severity:t?"low":"medium",file:e.file,line:e.line,subject:"External network call (Python)",message:"Added Python performs an external HTTP request that expands network reach.",recommendation:"Review the endpoint, request payload, and whether the call belongs in this change."}]}function detectPySecretExfil(e,t,n){if(!isPyExternalRequest(e.content)||!referencesPyEnvSecret(e.content)&&!py_capability_referencesSecretVariable(e.content,n)){return[]}return[{kind:"capability_echo.source_secret_exfil_pattern",surface:"source",severity:t?"medium":"high",file:e.file,line:e.line,subject:"Source secret exfiltration pattern (Python)",message:"Added Python sends environment-secret-shaped data to an external endpoint.",recommendation:"Do not send env secrets to external services unless the endpoint and payload are explicitly required."}]}function isPyExternalRequest(e){return/\b(?:requests|httpx)\.(?:get|post|put|delete|patch|head|options|request)\s*\(|\burllib(?:2)?\.(?:request\.)?urlopen\s*\(|\burlopen\s*\(|\burllib\.request\.urlretrieve\s*\(|\baiohttp\.ClientSession\s*\(/i.test(e)&&/(?:https?:\/\/|['"]https?:\/\/)/i.test(e)}function referencesPyEnvSecret(e){return/\bos\.environ\s*(?:\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\]|\.get\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"])/i.test(e)||/\bos\.getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]/i.test(e)}function py_capability_referencesSecretVariable(e,t){return[...t].some((t=>new RegExp(String.raw`\b${py_capability_escapeRegExp(t)}\b`).test(e)))}function py_capability_escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function detectPySubprocess(e,t){const n=/\bsubprocess\.(?:run|call|Popen|check_call|check_output|getoutput|getstatusoutput)\s*\(|\bos\.(?:system|popen|execv\w*|spawnv?\w*)\s*\(|\bcommands\.getoutput\s*\(|\bpty\.spawn\s*\(/i;if(!n.test(e.content)){return[]}return[{kind:"capability_echo.subprocess_spawn_added",surface:"source",severity:t?"low":"high",file:e.file,line:e.line,subject:"Subprocess spawn (Python)",message:"Added Python can spawn shell commands or subprocesses.",recommendation:"Confirm the command source is trusted and scoped to the task."}]}function detectPyDynamicExec(e,t){const n=/\beval\s*\(|\bexec\s*\(|\bcompile\s*\(|\b__import__\s*\(|\bimportlib\.import_module\s*\(/i;if(!n.test(e.content)){return[]}return[{kind:"capability_echo.dynamic_eval_added",surface:"source",severity:t?"medium":"critical",file:e.file,line:e.line,subject:"Dynamic code execution (Python)",message:"Added Python can evaluate dynamic code or import modules by name at runtime.",recommendation:"Avoid eval-style execution unless strictly required; never feed user input to these."}]}function detectPyUnsafeDeserialize(e,t){const n=/\bpickle\.(?:load|loads)\s*\(|\bmarshal\.(?:load|loads)\s*\(|\byaml\.load\s*\((?![^)]*Loader\s*=\s*(?:yaml\.)?SafeLoader)/i;if(!n.test(e.content)){return[]}return[{kind:"capability_echo.unsafe_deserialize_added",surface:"source",severity:t?"medium":"critical",file:e.file,line:e.line,subject:"Unsafe deserialization (Python)",message:"Added Python deserializes untrusted-shaped input (pickle / marshal / yaml.load).",recommendation:"Use yaml.safe_load and avoid pickle/marshal on data crossing trust boundaries."}]}function detectShellCapability(e){const t=[];for(const n of e){if(!(0,s.Z7)(n.file)||(0,s.w5)(n.content)){continue}t.push(...shell_capability_detectPipeToShell(n));t.push(...detectExternalDownload(n))}return t}function shell_capability_detectPipeToShell(e){if(!/(?:curl|wget|Invoke-WebRequest|iwr)[^\n|]*https?:\/\/[^\n|]*\|\s*(?:ba)?sh\b|iex\s*\(|Invoke-Expression/i.test(e.content)){return[]}return[{kind:"capability_echo.shell_pipe_to_shell",surface:"source",severity:"critical",file:e.file,line:e.line,subject:"Shell remote pipe-to-shell",message:"Added shell script downloads remote content and pipes it directly to a shell.",recommendation:"Replace remote pipe-to-shell with pinned, reviewable install steps."}]}function detectExternalDownload(e){if(!/\b(curl|wget|Invoke-WebRequest|iwr)\b[^\n]*https?:\/\//i.test(e.content)){return[]}return[{kind:"capability_echo.shell_external_download",surface:"source",severity:"medium",file:e.file,line:e.line,subject:"Shell external download",message:"Added shell script downloads content from an external URL.",recommendation:"Verify the URL, checksum or signature, and whether the download belongs in this change."}]}const x=/^\s*(?:actions|artifact-metadata|attestations|checks|code-quality|contents|deployments|discussions|id-token|issues|packages|pages|pull-requests|security-events|statuses)\s*:\s*write\b/i;function detectWorkflowPermissions(e,t={}){const n=[];const i=new Set(e.filter((e=>(0,s.Kr)(e.file)&&isPullRequestTargetLine(e.content))).map((e=>e.file)));const r=collectSecretEnvVars(e,t);for(const[e,n]of Object.entries(t)){if((0,s.Kr)(e)&&hasPullRequestTargetWorkflow(n)){i.add(e)}}for(const t of e){if(!(0,s.Kr)(t.file)){continue}n.push(...detectPullRequestTarget(t));n.push(...detectPullRequestHeadCheckoutOnTarget(t,i.has(t.file)));n.push(...detectSelfHostedRunner(t));n.push(...detectMutableActionRef(t));n.push(...detectWritePermissions(t));n.push(...detectExternalCurl(t));n.push(...detectSecretsInherit(t));n.push(...workflow_permissions_detectSecretExfil(t,r.get(t.file)??new Set));n.push(...detectDockerHostControl(t))}return n}function collectSecretEnvVars(e,t){const n=new Map;for(const t of e){if(!(0,s.Kr)(t.file)){continue}addSecretEnvVar(n,t.file,t.content)}for(const[e,i]of Object.entries(t)){if(!(0,s.Kr)(e)){continue}for(const t of i.split(/\r?\n/)){addSecretEnvVar(n,e,t)}}return n}function addSecretEnvVar(e,t,n){const i=n.match(/^\s*([A-Z_][A-Z0-9_]*)\s*:\s*.*\$\{\{\s*secrets\./i);if(!i){return}const s=e.get(t)??new Set;s.add(i[1]);e.set(t,s)}function detectWritePermissions(e){const t=e.content;if(!/permissions\s*:/i.test(t)&&!x.test(t)){return[]}if(x.test(t)){return[{kind:"capability_echo.workflow_permission_write",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions write permission",message:"Workflow grants repository or package write permissions.",recommendation:"Use the narrowest permission scope required for this job."}]}if(/^\s*permissions\s*:\s*(?:write|write-all|admin)\b/i.test(t)){return[{kind:"capability_echo.workflow_permission_write",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions broad write permission",message:"Workflow grants broad write or admin permissions.",recommendation:"Prefer explicit per-resource permissions instead of top-level write/admin."}]}return[]}function detectPullRequestTarget(e){if(!isPullRequestTargetLine(e.content)){return[]}return[{kind:"capability_echo.workflow_pull_request_target",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions pull_request_target trigger",message:"Workflow runs on pull_request_target, which can expose elevated token or secret context to PR-triggered automation.",recommendation:"Use pull_request unless elevated base-repository context is required; never run untrusted PR code with pull_request_target privileges."}]}function detectPullRequestHeadCheckoutOnTarget(e,t){if(!t||!isPullRequestHeadCheckoutLine(e.content)){return[]}return[{kind:"capability_echo.workflow_pr_head_checkout_on_target",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions PR-head checkout under pull_request_target",message:"Workflow checks out pull request head code in a pull_request_target workflow.",recommendation:"Use pull_request for untrusted PR code, or avoid checking out PR head code under pull_request_target."}]}function isPullRequestTargetLine(e){return/^\s*pull_request_target\s*:/i.test(e)}function hasPullRequestTargetWorkflow(e){return e.split(/\r?\n/).some(isPullRequestTargetLine)}function isPullRequestHeadCheckoutLine(e){return/^\s*(?:ref|repository)\s*:\s*.*github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)/i.test(e)}function detectSelfHostedRunner(e){if(!/^\s*runs-on\s*:\s*(?:.*\bself-hosted\b|.*\[\s*self-hosted\b)/i.test(e.content)&&!/^\s*-\s*self-hosted\s*(?:#.*)?$/i.test(e.content)){return[]}return[{kind:"capability_echo.workflow_self_hosted_runner",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions self-hosted runner",message:"Workflow runs on a self-hosted runner, which can expand PR-triggered automation into private infrastructure.",recommendation:"Use GitHub-hosted runners for untrusted PR code, or isolate self-hosted runners with strict labels, permissions, and cleanup."}]}function detectMutableActionRef(e){const t=extractWorkflowUsesRef(e.content);if(!t||isLocalActionRef(t)||/^docker:\/\//i.test(t)){return[]}const n=t.lastIndexOf("@");if(n===-1){return[]}const i=t.slice(n+1);if(!isMutableActionVersionRef(i)){return[]}return[{kind:"capability_echo.workflow_mutable_action_ref",surface:"workflow",severity:"medium",file:e.file,line:e.line,subject:"GitHub Actions mutable action reference",message:"Workflow uses a mutable remote action reference.",recommendation:"Pin third-party actions to a reviewed commit SHA before merge."}]}function extractWorkflowUsesRef(e){return e.match(/^\s*(?:-\s*)?uses\s*:\s*['"]?([^'"\s#]+)['"]?/i)?.[1]}function isLocalActionRef(e){return e.startsWith("./")||e.startsWith("../")||e.startsWith("/")}function isMutableActionVersionRef(e){return/^(main|master|trunk|develop|dev|latest|head)$/i.test(e)}function detectExternalCurl(e){if(!/\b(curl|wget|Invoke-WebRequest|fetch\s*\()/i.test(e.content)){return[]}return[{kind:"capability_echo.workflow_external_curl",surface:"workflow",severity:"medium",file:e.file,line:e.line,subject:"Workflow external request",message:"Workflow step performs an external network request.",recommendation:"Verify the URL, payload, and whether the request is necessary in CI."}]}function detectSecretsInherit(e){if(!/^\s*secrets\s*:\s*inherit\s*(?:#.*)?$/i.test(e.content)){return[]}return[{kind:"capability_echo.workflow_secrets_inherit",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions inherited secrets",message:"Workflow passes all caller secrets to a reusable workflow.",recommendation:"Pass only explicit secrets required by the reusable workflow."}]}function workflow_permissions_detectSecretExfil(e,t){const n=e.content;const i=/\$\{\{\s*secrets\.|\$\{?\s*secrets\.|env\.[A-Z0-9_]+/i.test(n)||[...t].some((e=>referencesShellVariable(n,e)));const s=/\b(curl|wget|Invoke-WebRequest|fetch\s*\()/i.test(n);const r=/\|\s*(bash|sh|powershell|pwsh)/i.test(n);if(!i||!s){return[]}return[{kind:"capability_echo.workflow_secret_exfil_pattern",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"Workflow secret exfiltration pattern",message:"Workflow step references secrets or env values alongside an external request or shell pipe.",recommendation:"Review whether secrets could leave the runner through this step."}]}function referencesShellVariable(e,t){const n=workflow_permissions_escapeRegExp(t);return new RegExp(String.raw`(?:\$\{${n}\}|\$${n}\b|%${n}%)`).test(e)}function workflow_permissions_escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function detectDockerHostControl(e){const t=[];const n=e.content;if(/\/var\/run\/docker\.sock(?::\/var\/run\/docker\.sock)?/i.test(n)){t.push({kind:"capability_echo.workflow_docker_socket_mount",surface:"workflow",severity:"critical",file:e.file,line:e.line,subject:"Workflow Docker socket mount",message:"Workflow mounts the host Docker socket, which can grant control over the runner host.",recommendation:"Avoid Docker socket mounts in CI unless the job is isolated and the image/commands are trusted."})}if(/\bdocker\s+run\b.*\s--privileged(?:\s|$)/i.test(n)){t.push({kind:"capability_echo.workflow_privileged_container",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"Workflow privileged container",message:"Workflow runs a privileged container, expanding kernel and device-level access in CI.",recommendation:"Use the narrowest container privileges required, and avoid privileged mode for agent-run code."})}return t}var $=i(665);async function runCapabilityDiff(e){const t=e.mode==="directories"?await(0,_.BH)(e.oldRoot,e.newRoot):await(0,_._u)(e.repo,e.base,e.head);const n=e.mode==="directories"?{mode:"directories",oldRoot:e.oldRoot,newRoot:e.newRoot}:{mode:"git",repo:e.repo,base:e.base,head:e.head};const[i,s]=await Promise.all([detectPackageScripts(n),detectPackageDeps(n)]);const r=[...detectWorkflowPermissions(t.addedLines,t.newFileContents),...detectDockerfileCapability(t.addedLines),...detectJsCapability(t.addedLines,t.newFileContents),...detectPyCapability(t.addedLines,t.newFileContents),...detectShellCapability(t.addedLines),...i,...s];return(0,$.L)(r,t)}},924:(t,n,i)=>{i.d(n,{rd:()=>GitDiffSetupError,BH:()=>collectDirectoryDiff,_u:()=>collectGitDiff,Eh:()=>listGitChangedFiles,kf:()=>listPackageJsonFiles,p9:()=>readFileAtGitRef});const s=e(import.meta.url)("node:child_process");var r=i(455);var o=i(760);const a=e(import.meta.url)("node:util");var c=i(431);const l=(0,a.promisify)(s.execFile);const u=["source","package","workflow","container"];class GitDiffSetupError extends Error{base;head;constructor(e,t,n){super(e);this.base=t;this.head=n;this.name="GitDiffSetupError"}}async function collectDirectoryDiff(e,t){const n=await listScannableFiles(t);const i=[];const s=new Set;const a={};for(const c of n){const n=(0,o.join)(e,c);const l=(0,o.join)(t,c);const u=await(0,r.readFile)(l,"utf8");const f=await runGitNoIndexDiff(n,l);if(f.trim()){s.add(c);a[c]=u;i.push(...parseUnifiedDiff(f,c));continue}let p="";try{p=await(0,r.readFile)(n,"utf8")}catch{p=""}if(p===u){continue}s.add(c);a[c]=u;if(!p){i.push(...allLinesAsAdded(c,u))}}return{addedLines:i,changedFileCount:s.size,scannedSurfaces:surfacesForFiles([...s]),newFileContents:a}}async function collectGitDiff(e,t,n){const i=await gitRefExists(e,t);const s=await gitRefExists(e,n);if(!i||!s){throw new GitDiffSetupError(`CapabilityEcho could not compare base '${t}' and head '${n}'.`,t,n)}const r=await listGitChangedFiles(e,t,n);const o=r.filter(c.Iy);if(o.length===0){return{addedLines:[],changedFileCount:0,scannedSurfaces:[],newFileContents:{}}}const{stdout:a}=await l("git",["-C",e,"diff","-U0",`${t}..${n}`,"--",...o],{encoding:"utf8",maxBuffer:20*1024*1024});const u=await readChangedFilesAtRef(e,n,o);return{addedLines:parseUnifiedDiff(a).map((e=>({...e,file:normalizeGitDiffPath(e.file)}))),changedFileCount:o.length,scannedSurfaces:surfacesForFiles(o),newFileContents:u}}function parseUnifiedDiff(e,t){const n=[];let i="";let s=0;for(const r of e.split(/\r?\n/)){if(r.startsWith("+++ ")){const e=r.slice(4).trim();if(e.startsWith("b/")){i=e.slice(2)}else if(e==="/dev/null"){i=""}else{i=e}continue}if(r.startsWith("@@")){const e=r.match(/\+(\d+)(?:,(\d+))?/);s=e?Number.parseInt(e[1],10):0;continue}if(!i||i==="/dev/null"){continue}if(r.startsWith("+")&&!r.startsWith("+++")){n.push({file:(t??normalizeGitDiffPath(i)).replace(/\\/g,"/"),line:s,content:r.slice(1)});s+=1;continue}if(r.startsWith("-")&&!r.startsWith("---")){continue}if(r.startsWith(" ")||r.startsWith("\\")){s+=1}}return n}async function listScannableFiles(e,t=""){const n=await(0,r.readdir)((0,o.join)(e,t),{withFileTypes:true});const i=[];for(const s of n){if(s.name==="node_modules"||s.name===".git"){continue}const n=t?`${t}/${s.name}`:s.name;if(s.isDirectory()){i.push(...await listScannableFiles(e,n));continue}if((0,c.Iy)(n)){i.push(n.replace(/\\/g,"/"))}}return i}async function listGitChangedFiles(e,t,n){const{stdout:i}=await l("git",["-C",e,"diff","--name-only",`${t}..${n}`],{encoding:"utf8",maxBuffer:10*1024*1024});return i.split(/\r?\n/).map((e=>e.trim())).filter(Boolean).map((e=>e.replace(/\\/g,"/")))}async function runGitNoIndexDiff(e,t){try{const{stdout:n}=await l("git",["diff","--no-index","-U0",e,t],{encoding:"utf8",maxBuffer:10*1024*1024});return n}catch(e){if(isExecError(e)&&typeof e.stdout==="string"){return e.stdout}return""}}function allLinesAsAdded(e,t){const n=t.split(/\r?\n/);return n.map(((t,n)=>({file:e.replace(/\\/g,"/"),line:n+1,content:t})))}async function gitRefExists(e,t){try{await l("git",["-C",e,"rev-parse","--verify",`${t}^{commit}`]);return true}catch(e){if(isExecError(e)){return false}throw e}}function surfacesForFiles(e){const t=new Set;for(const n of e){const e=(0,c.qk)(n);if(e){t.add(e)}}return u.filter((e=>t.has(e)))}function isExecError(e){return e instanceof Error&&"code"in e}function normalizeGitDiffPath(e){const t=e.replace(/\\/g,"/");const n=["/src/","/.github/workflows/","/package.json"];for(const e of n){const n=t.lastIndexOf(e);if(n>=0){return t.slice(n+1)}}if(t.endsWith("package.json")){return"package.json"}return t.replace(/^[a-z]:\//i,"").replace(/^b\//,"")}async function readFileAtGitRef(e,t,n){try{const{stdout:i}=await l("git",["-C",e,"show",`${t}:${n}`],{encoding:"utf8",maxBuffer:10*1024*1024});return i}catch(e){if(isExecError(e)){return null}throw e}}async function readChangedFilesAtRef(e,t,n){const i=await Promise.all(n.map((async n=>{const i=await readFileAtGitRef(e,t,n);return i===null?undefined:[n,i]})));return Object.fromEntries(i.filter((e=>e!==undefined)))}async function listPackageJsonFiles(e,t=""){const n=await(0,r.readdir)((0,o.join)(e,t),{withFileTypes:true});const i=[];for(const s of n){if(s.name==="node_modules"||s.name===".git"){continue}const n=t?`${t}/${s.name}`:s.name;if(s.isDirectory()){i.push(...await listPackageJsonFiles(e,n));continue}if(s.name==="package.json"){i.push(n.replace(/\\/g,"/"))}}return i}function relativeFromRoots(e,t){return relative(e,t).replace(/\\/g,"/")}},431:(e,t,n)=>{n.d(t,{Iy:()=>isScannable,Kr:()=>isWorkflowFile,Mn:()=>isJsFile,UD:()=>isPyFile,Z7:()=>isShellFile,hl:()=>isTestFile,pZ:()=>isDockerfile,qk:()=>surfaceForPath,w5:()=>isCommentLine,xr:()=>isPackageJsonFile});const i=new Set([".mcp.json",".cursor/mcp.json",".vscode/mcp.json",".codeium/windsurf/mcp_config.json",".claude/settings.json",".codex/config.toml","AGENTS.md"]);function normalizeRelativePath(e){return e.replace(/\\/g,"/")}function isExcluded(e){const t=normalizeRelativePath(e);if(i.has(t)){return true}return t.startsWith(".cursor/rules/")}function isScannable(e){return surfaceForPath(e)!==undefined}function surfaceForPath(e){const t=normalizeRelativePath(e);if(isExcluded(t)){return undefined}if(t==="package.json"||t.endsWith("/package.json")){return"package"}if(t.startsWith(".github/workflows/")&&/\.(ya?ml)$/i.test(t)){return"workflow"}if(isDockerfile(t)){return"container"}if(isJsFile(t)||isPyFile(t)||isShellFile(t)){return"source"}return undefined}function isTestFile(e){const t=normalizeRelativePath(e);if(t.includes("__tests__/")||t.includes("/tests/")){return true}if(/(^|\/)test_[^/]+\.py$/i.test(t)||/_test\.py$/i.test(t)){return true}return/\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(t)}function isCommentLine(e){const t=e.trim();return t.startsWith("//")||t.startsWith("/*")||t.startsWith("*")||t.startsWith("*/")||t.startsWith("#")}function isWorkflowFile(e){const t=normalizeRelativePath(e);return t.startsWith(".github/workflows/")&&/\.(ya?ml)$/i.test(t)}function isPackageJsonFile(e){const t=normalizeRelativePath(e);return t==="package.json"||t.endsWith("/package.json")}function isJsFile(e){const t=normalizeRelativePath(e);return/\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(t)}function isPyFile(e){const t=normalizeRelativePath(e);return/\.(py|pyw)$/i.test(t)}function isShellFile(e){const t=normalizeRelativePath(e);return/\.(sh|bash|zsh|ps1|psm1)$/i.test(t)}function isDockerfile(e){const t=normalizeRelativePath(e);const n=t.split("/").pop()??t;return/^Dockerfile(?:\..+)?$/i.test(n)}},665:(e,t,n)=>{n.d(t,{B:()=>renderReport,L:()=>createReport});const i=["AI-agent config"];const s=["critical","high","medium","low"];const r={source:"source code",package:"package manifests",workflow:"GitHub workflows",container:"container builds"};const o={none:0,low:1,medium:2,high:3,critical:4};const a={"capability_echo.external_fetch_added":"external network fetch calls","capability_echo.source_secret_exfil_pattern":"source secret exfiltration patterns","capability_echo.subprocess_spawn_added":"subprocess or shell spawn calls","capability_echo.dynamic_eval_added":"dynamic code execution","capability_echo.shell_pipe_to_shell":"shell pipe-to-shell downloads","capability_echo.shell_external_download":"shell external downloads","capability_echo.dockerfile_remote_add":"Dockerfile remote ADD instructions","capability_echo.dockerfile_pipe_to_shell":"Dockerfile pipe-to-shell builds","capability_echo.workflow_permission_write":"GitHub Actions write permissions","capability_echo.workflow_pull_request_target":"GitHub Actions pull_request_target triggers","capability_echo.workflow_pr_head_checkout_on_target":"GitHub Actions PR-head checkout under pull_request_target","capability_echo.workflow_self_hosted_runner":"GitHub Actions self-hosted runners","capability_echo.workflow_mutable_action_ref":"GitHub Actions mutable action references","capability_echo.workflow_secrets_inherit":"GitHub Actions inherited secrets","capability_echo.workflow_external_curl":"workflow external network requests","capability_echo.workflow_secret_exfil_pattern":"workflow secret exfiltration patterns","capability_echo.workflow_docker_socket_mount":"workflow Docker socket mounts","capability_echo.workflow_privileged_container":"workflow privileged containers","capability_echo.lifecycle_script_added":"npm lifecycle scripts","capability_echo.script_pipe_to_shell":"pipe-to-shell install scripts","capability_echo.script_network_command":"network or publish npm scripts","capability_echo.high_capability_dep_added":"high-capability dependency additions","capability_echo.telemetry_dep_added":"telemetry dependency additions","capability_echo.unsafe_deserialize_added":"unsafe deserialization"};function createReport(e,t){return{rating:rateFindings(e),findingCount:e.length,changedFileCount:t.changedFileCount,scannedSurfaces:t.scannedSurfaces,excludedSurfaces:[...i],surfaceSummary:buildSurfaceSummary(e),severitySummary:buildSeveritySummary(e),capabilitySummary:buildCapabilitySummary(e),topRecommendations:buildTopRecommendations(e),findings:e}}function renderReport(e,t){if(t==="json"){return`${JSON.stringify(e,null,2)}\n`}if(t==="markdown"){return renderMarkdown(e)}if(t==="github"){return renderGithubAnnotations(e)}return renderText(e)}function buildCapabilitySummary(e){const t=new Set;for(const n of e){t.add(a[n.kind]??n.kind)}return[...t]}function buildTopRecommendations(e){const t=new Set;const n=e.map(((e,t)=>({finding:e,index:t}))).sort(((e,t)=>{const n=o[t.finding.severity]-o[e.finding.severity];return n===0?e.index-t.index:n}));for(const{finding:e}of n){t.add(e.recommendation);if(t.size===3){break}}return[...t]}function rateFindings(e){let t="none";for(const n of e){if(o[n.severity]>o[t]){t=n.severity}}return t}function renderMarkdown(e){const t=[`# CapabilityEcho capability drift: ${e.rating.toUpperCase()}`,""];t.push(`Scanned executable surfaces: ${formatSurfaces(e.scannedSurfaces)}.`);t.push(`Excluded surfaces: ${e.excludedSurfaces.join(", ")}.`,"");if(e.findings.length===0){t.push("No code or workflow capability drift findings.");return`${t.join("\n")}\n`}t.push(`This diff scanned ${e.changedFileCount} changed file${e.changedFileCount===1?"":"s"}.`);t.push(`CapabilityEcho found ${e.findingCount} finding${e.findingCount===1?"":"s"}.`,"");if(e.topRecommendations.length>0){t.push("## Top recommendations","");for(const n of e.topRecommendations){t.push(`- ${n}`)}t.push("")}t.push("## Review summary","");t.push("| Surface | Findings |");t.push("| --- | ---: |");for(const n of["source","package","workflow","container"]){t.push(`| ${r[n]} | ${e.surfaceSummary[n]} |`)}t.push("");t.push("| Severity | Findings |");t.push("| --- | ---: |");for(const n of s){t.push(`| ${capitalize(n)} | ${e.severitySummary[n]} |`)}t.push("");if(e.capabilitySummary.length>0){t.push("## Capability summary","");for(const n of e.capabilitySummary){t.push(`- ${n}`)}t.push("")}for(const n of s){const i=e.findings.filter((e=>e.severity===n));if(i.length===0){continue}t.push(`## ${capitalize(n)}`,"");for(const e of i){t.push(`- **${e.subject}** [${r[e.surface]}] (${e.file}): ${e.message}`);t.push(` Recommendation: ${e.recommendation}`)}t.push("")}return`${t.join("\n").trimEnd()}\n`}function renderText(e){const t=[`CapabilityEcho capability drift: ${e.rating.toUpperCase()}`];t.push(`Scanned executable surfaces: ${formatSurfaces(e.scannedSurfaces)}.`);t.push(`Excluded surfaces: ${e.excludedSurfaces.join(", ")}.`);if(e.capabilitySummary.length>0){t.push(`Signals: ${e.capabilitySummary.join(", ")}`)}if(e.topRecommendations.length>0){t.push(`Top recommendations: ${e.topRecommendations.join(" | ")}`)}for(const n of e.findings){t.push(`[${n.severity.toUpperCase()}] ${n.subject} (${r[n.surface]}): ${n.message}`)}if(e.findings.length===0){t.push("No code or workflow capability drift findings.")}return`${t.join("\n")}\n`}function renderGithubAnnotations(e){if(e.findings.length===0){return""}return e.findings.map((e=>{const t=`CapabilityEcho ${e.severity} ${r[e.surface]} capability drift`;const n=`${e.message} Recommendation: ${e.recommendation}`;const i=[`file=${escapeProperty(e.file)}`];if(e.line&&e.line>0){i.push(`line=${e.line}`)}i.push(`title=${escapeProperty(t)}`);return`::warning ${i.join(",")}::${escapeMessage(n)}`})).join("\n")+"\n"}function escapeMessage(e){return e.replaceAll("%","%25").replaceAll("\r","%0D").replaceAll("\n","%0A")}function escapeProperty(e){return escapeMessage(e).replaceAll(":","%3A").replaceAll(",","%2C")}function capitalize(e){return`${e.slice(0,1).toUpperCase()}${e.slice(1)}`}function buildSurfaceSummary(e){return{source:e.filter((e=>e.surface==="source")).length,package:e.filter((e=>e.surface==="package")).length,workflow:e.filter((e=>e.surface==="workflow")).length,container:e.filter((e=>e.surface==="container")).length}}function buildSeveritySummary(e){return{critical:e.filter((e=>e.severity==="critical")).length,high:e.filter((e=>e.severity==="high")).length,medium:e.filter((e=>e.severity==="medium")).length,low:e.filter((e=>e.severity==="low")).length}}function formatSurfaces(e){if(e.length===0){return"none"}return e.map((e=>r[e])).join(", ")}},455:t=>{t.exports=e(import.meta.url)("node:fs/promises")},760:t=>{t.exports=e(import.meta.url)("node:path")}};var n={};function __nccwpck_require__(e){var i=n[e];if(i!==undefined){return i.exports}var s=n[e]={exports:{}};var r=true;try{t[e](s,s.exports,__nccwpck_require__);r=false}finally{if(r)delete n[e]}return s.exports}(()=>{var e=typeof Symbol==="function"?Symbol("webpack queues"):"__webpack_queues__";var t=typeof Symbol==="function"?Symbol("webpack exports"):"__webpack_exports__";var n=typeof Symbol==="function"?Symbol("webpack error"):"__webpack_error__";var resolveQueue=e=>{if(e&&e.d<1){e.d=1;e.forEach((e=>e.r--));e.forEach((e=>e.r--?e.r++:e()))}};var wrapDeps=i=>i.map((i=>{if(i!==null&&typeof i==="object"){if(i[e])return i;if(i.then){var s=[];s.d=0;i.then((e=>{r[t]=e;resolveQueue(s)}),(e=>{r[n]=e;resolveQueue(s)}));var r={};r[e]=e=>e(s);return r}}var o={};o[e]=e=>{};o[t]=i;return o}));__nccwpck_require__.a=(i,s,r)=>{var o;r&&((o=[]).d=-1);var a=new Set;var c=i.exports;var l;var u;var f;var p=new Promise(((e,t)=>{f=t;u=e}));p[t]=c;p[e]=e=>(o&&e(o),a.forEach(e),p["catch"]((e=>{})));i.exports=p;s((i=>{l=wrapDeps(i);var s;var getResult=()=>l.map((e=>{if(e[n])throw e[n];return e[t]}));var r=new Promise((t=>{s=()=>t(getResult);s.r=0;var fnQueue=e=>e!==o&&!a.has(e)&&(a.add(e),e&&!e.d&&(s.r++,e.push(s)));l.map((t=>t[e](fnQueue)))}));return s.r?r:getResult()}),(e=>(e?f(p[n]=e):u(c),resolveQueue(o))));o&&o.d<0&&(o.d=0)}})();(()=>{__nccwpck_require__.n=e=>{var t=e&&e.__esModule?()=>e["default"]:()=>e;__nccwpck_require__.d(t,{a:t});return t}})();(()=>{__nccwpck_require__.d=(e,t)=>{for(var n in t){if(__nccwpck_require__.o(t,n)&&!__nccwpck_require__.o(e,n)){Object.defineProperty(e,n,{enumerable:true,get:t[n]})}}}})();(()=>{__nccwpck_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})();if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var i=__nccwpck_require__(929);i=await i;var s=i.g;export{s as mainAction}; \ No newline at end of file +import{createRequire as e}from"module";var t={929:(e,t,n)=>{n.a(e,(async(e,i)=>{try{n.d(t,{g:()=>mainAction});var s=n(455);var r=n.n(s);var o=n(499);var a=n(924);var c=n(665);async function mainAction(e=process.env){const t=getInput(e,"repo")||e.GITHUB_WORKSPACE||process.cwd();const n=await readEvent(e);const i=getInput(e,"base")||getDefaultBase(e,n);const s=getInput(e,"head")||getDefaultHead(e,n);const r=getInput(e,"fail-on")||"none";const l=r.toLowerCase();if(!i||!s){writeError("CapabilityEcho needs base and head refs. Pass base/head inputs or run on pull_request with actions/checkout fetch-depth: 0.");return 2}if(!isRating(l)){writeError(`Invalid fail-on value '${r}'. Use none, low, medium, high, or critical.`);return 2}let u;try{u=await(0,o.X)({mode:"git",repo:t,base:i,head:s})}catch(e){if(e instanceof a.rd){writeError(`CapabilityEcho could not compare base '${e.base}' and head '${e.head}'. Ensure actions/checkout uses fetch-depth: 0, or pass refs that exist in the checkout through the \`base\` and \`head\` inputs.`);return 2}throw e}const f=(0,c.B1)(u,"markdown");const p=(0,c.B1)(u,"json");const d=JSON.stringify({rating:u.rating,hasFindings:u.findingCount>0,findingCount:u.findingCount,changedFileCount:u.changedFileCount,surfaceSummary:u.surfaceSummary,severitySummary:u.severitySummary,capabilitySummary:u.capabilitySummary,topRecommendations:u.topRecommendations});process.stdout.write(f);process.stdout.write((0,c.B1)(u,"github"));await appendIfSet(e.GITHUB_STEP_SUMMARY,f);await writeOutput(e,"rating",u.rating);await writeOutput(e,"has-findings",String(u.findingCount>0));await writeOutput(e,"finding-count",String(u.findingCount));await writeOutput(e,"changed-file-count",String(u.changedFileCount));await writeOutput(e,"surface-summary",JSON.stringify(u.surfaceSummary));await writeOutput(e,"severity-summary",JSON.stringify(u.severitySummary));await writeOutput(e,"capability-summary",JSON.stringify(u.capabilitySummary));await writeOutput(e,"top-recommendations",JSON.stringify(u.topRecommendations));await writeOutput(e,"adoption-evidence",d);await writeOutput(e,"report-markdown",f);await writeOutput(e,"report-json",p);if(c.qs[l]>0&&c.qs[u.rating]>=c.qs[l]){writeError(`CapabilityEcho capability drift rating ${u.rating} meets fail-on threshold ${l}.`);return 1}return 0}function getInput(e,t){const n=e[`INPUT_${t.replace(/ /g,"_").toUpperCase()}`];const i=e[`INPUT_${t.replace(/[- ]/g,"_").toUpperCase()}`];return(n||i||"").trim()}async function readEvent(e){if(!e.GITHUB_EVENT_PATH){return{}}try{const t=await(0,s.readFile)(e.GITHUB_EVENT_PATH,"utf8");const n=JSON.parse(t);return isRecord(n)?n:{}}catch{return{}}}function getDefaultBase(e,t){const n=t.pull_request;if(isRecord(n)&&isRecord(n.base)&&typeof n.base.sha==="string"){return n.base.sha}if(typeof t.before==="string"){return t.before}return e.DEFAULT_BASE||""}function getDefaultHead(e,t){const n=t.pull_request;if(isRecord(n)&&isRecord(n.head)&&typeof n.head.sha==="string"){return n.head.sha}if(typeof t.after==="string"){return t.after}return e.DEFAULT_HEAD||e.GITHUB_SHA||""}async function writeOutput(e,t,n){if(!e.GITHUB_OUTPUT){return}if(n.includes("\n")||n.includes("\r")){const i=outputDelimiter(t,n);const r=n.endsWith("\n")?n:`${n}\n`;await(0,s.appendFile)(e.GITHUB_OUTPUT,`${t}<<${i}\n${r}${i}\n`,"utf8");return}await(0,s.appendFile)(e.GITHUB_OUTPUT,`${t}=${n}\n`,"utf8")}function outputDelimiter(e,t){const n=e.replace(/[^A-Za-z0-9_]+/g,"_");let i=`capabilityecho_${n}_EOF`;let s=1;while(t.includes(i)){i=`capabilityecho_${n}_EOF_${s}`;s+=1}return i}async function appendIfSet(e,t){if(!e){return}await(0,s.appendFile)(e,t,"utf8")}function writeError(e){process.stdout.write(`::error::${escapeMessage(e)}\n`)}function escapeMessage(e){return e.replaceAll("%","%25").replaceAll("\r","%0D").replaceAll("\n","%0A")}function isRating(e){return e==="none"||e==="low"||e==="medium"||e==="high"||e==="critical"}function isRecord(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}if(process.argv[1]?.endsWith("action.js")){process.exitCode=await mainAction()}i()}catch(l){i(l)}}),1)},499:(t,n,i)=>{i.d(n,{X:()=>runCapabilityDiff});var s=i(431);function detectDockerfileCapability(e){const t=[];for(const n of e){if(!(0,s.pZ)(n.file)||(0,s.w5)(n.content)){continue}t.push(...detectRemoteAdd(n));t.push(...detectPipeToShell(n))}return t}function detectRemoteAdd(e){if(!/^\s*ADD\s+https?:\/\//i.test(e.content)){return[]}return[{kind:"capability_echo.dockerfile_remote_add",surface:"container",severity:"high",file:e.file,line:e.line,subject:"Dockerfile remote ADD",message:"Dockerfile adds remote content during image build, expanding build-time network reach.",recommendation:"Download pinned artifacts with checksum verification, or vendor reviewed files into the repository."}]}function detectPipeToShell(e){if(!/^\s*RUN\b.*(?:curl|wget)[^\n|]*https?:\/\/[^\n|]*\|\s*(?:ba)?sh\b/i.test(e.content)){return[]}return[{kind:"capability_echo.dockerfile_pipe_to_shell",surface:"container",severity:"critical",file:e.file,line:e.line,subject:"Dockerfile pipe-to-shell",message:"Dockerfile downloads remote content and pipes it directly to a shell during image build.",recommendation:"Replace remote pipe-to-shell with pinned, reviewable build steps and checksum verification."}]}function detectJsCapability(e,t={}){const n=[];const i=collectSecretVariables(e,t);for(const t of e){if(!(0,s.Mn)(t.file)||(0,s.w5)(t.content)){continue}const e=(0,s.hl)(t.file);n.push(...detectFetch(t,e));n.push(...detectSecretExfil(t,e,i.get(t.file)??new Set));n.push(...detectSubprocess(t,e));n.push(...detectDynamicEval(t,e))}return n}function collectSecretVariables(e,t){const n=new Map;for(const t of e){if(!(0,s.Mn)(t.file)){continue}addSecretVariable(n,t.file,t.content)}for(const[e,i]of Object.entries(t)){if(!(0,s.Mn)(e)){continue}for(const t of i.split(/\r?\n/)){addSecretVariable(n,e,t)}}return n}function addSecretVariable(e,t,n){const i=n.match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*process\.env\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b/i);if(!i){return}const s=e.get(t)??new Set;s.add(i[1]);e.set(t,s)}function detectFetch(e,t){if(!/(?:fetch\s*\(|axios\.(?:get|post|put|delete|patch|request)\s*\(|got\s*\()/i.test(e.content)){return[]}if(!/(?:https?:\/\/|['"]https?:\/\/)/i.test(e.content)){return[]}if(/(?:fetch\s*\(\s*['"`]\/|axios\.(?:get|post|put|delete|patch|request)\s*\(\s*['"`]\/)/i.test(e.content)){return[]}return[{kind:"capability_echo.external_fetch_added",surface:"source",severity:t?"low":"medium",file:e.file,line:e.line,subject:"External network fetch",message:"Added code performs an external HTTP request that expands network reach.",recommendation:"Review the endpoint, data sent, and whether the request belongs in this change."}]}function detectSecretExfil(e,t,n){if(!isExternalHttpRequest(e.content)||!referencesEnvSecret(e.content)&&!referencesSecretVariable(e.content,n)){return[]}return[{kind:"capability_echo.source_secret_exfil_pattern",surface:"source",severity:t?"medium":"high",file:e.file,line:e.line,subject:"Source secret exfiltration pattern",message:"Added source code sends environment-secret-shaped data to an external endpoint.",recommendation:"Do not send env secrets to external services unless the endpoint and payload are explicitly required."}]}function isExternalHttpRequest(e){return/(?:fetch\s*\(|axios\.(?:get|post|put|delete|patch|request)\s*\(|got\s*\()/i.test(e)&&/(?:https?:\/\/|['"]https?:\/\/)/i.test(e)}function referencesEnvSecret(e){return/\bprocess\.env\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b/i.test(e)}function referencesSecretVariable(e,t){return[...t].some((t=>new RegExp(String.raw`\b${escapeRegExp(t)}\b`).test(e)))}function escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function detectSubprocess(e,t){if(!/(?:child_process|execSync\s*\(|exec\s*\(|spawnSync\s*\(|spawn\s*\(|Bun\.spawn\s*\()/i.test(e.content)){return[]}return[{kind:"capability_echo.subprocess_spawn_added",surface:"source",severity:t?"low":"high",file:e.file,line:e.line,subject:"Subprocess spawn",message:"Added code can spawn shell commands or subprocesses.",recommendation:"Confirm the command source is trusted and scoped to the task."}]}function detectDynamicEval(e,t){if(!/(?:\beval\s*\(|new\s+Function\s*\(|vm\.runInNewContext\s*\()/i.test(e.content)){return[]}return[{kind:"capability_echo.dynamic_eval_added",surface:"source",severity:t?"medium":"critical",file:e.file,line:e.line,subject:"Dynamic code execution",message:"Added code can evaluate dynamic JavaScript at runtime.",recommendation:"Avoid eval-style execution unless strictly required and heavily constrained."}]}var r=i(455);var o=i(760);const a=e(import.meta.url)("node:crypto");const c=null&&["low","medium","high","critical"];const l=null&&["scope_trail","policy_mesh","capability_echo","task_bound","session_trail"];function isSeverity(e){return typeof e==="string"&&c.includes(e)}function isToolKind(e){return typeof e==="string"&&l.includes(e)}function kind(e,t){if(!/^[a-z0-9_]+$/.test(t)){throw new Error(`agent-gov-core/kind: name '${t}' must match [a-z0-9_]+ (kebab, camelCase, and dots are rejected)`)}return`${e}.${t}`}const u=/^(scope_trail|policy_mesh|capability_echo|task_bound|session_trail)\.[a-z0-9_]+$/;function isNamespacedKind(e){return typeof e==="string"&&u.test(e)}function createFinding(e){const t={tool:e.tool,kind:kind(e.tool,e.name),severity:e.severity,message:e.message};if(e.detail!==undefined)t.detail=e.detail;if(e.location!==undefined)t.location=e.location;if(e.salientKey!==undefined)t.salientKey=e.salientKey;if(e.data!==undefined)t.data=e.data;t.fingerprint=e.fingerprint??fingerprintFinding(t);return t}function fingerprintFinding(e){const t=e.location?.file?.replace(/\\/g,"/")??"";const n=[e.kind,t,e.location?.line??"",e.location?.column??""];if(e.salientKey!==undefined){n.push(e.salientKey)}return createHash("sha256").update(n.join("|")).digest("hex").slice(0,16)}const f=new Set(["tool","kind","severity","message","detail","location","fingerprint","salientKey","data"]);const p=new Set(["file","line","column","endLine","endColumn"]);function validateFinding(e){const t=[];if(e===null||typeof e!=="object"||Array.isArray(e)){return{ok:false,errors:["finding must be a plain object"]}}const n=e;if(!isToolKind(n.tool))t.push(`tool must be one of: ${l.join(", ")}`);if(!isNamespacedKind(n.kind))t.push("kind must match '.' (e.g. 'scope_trail.permission_allow_widened')");if(!isSeverity(n.severity))t.push(`severity must be one of: ${c.join(", ")}`);if(typeof n.message!=="string"||n.message.length===0)t.push("message must be a non-empty string");if(isToolKind(n.tool)&&isNamespacedKind(n.kind)&&!n.kind.startsWith(`${n.tool}.`)){t.push(`kind '${n.kind}' must start with tool '${n.tool}.'`)}if(n.detail!==undefined&&typeof n.detail!=="string"){t.push("detail must be a string when present")}if(n.fingerprint!==undefined&&typeof n.fingerprint!=="string"){t.push("fingerprint must be a string when present")}if(n.salientKey!==undefined&&typeof n.salientKey!=="string"){t.push("salientKey must be a string when present")}if(n.data!==undefined&&(n.data===null||typeof n.data!=="object"||Array.isArray(n.data))){t.push("data must be an object when present")}if(n.location!==undefined){t.push(...validateLocation(n.location))}for(const e of Object.keys(n)){if(!f.has(e))t.push(`unknown property: ${e}`)}return{ok:t.length===0,errors:t}}function validateLocation(e){const t=[];if(e===null||typeof e!=="object"||Array.isArray(e)){return["location must be an object when present"]}const n=e;if(typeof n.file!=="string"||n.file.length===0){t.push("location.file must be a non-empty string")}for(const e of["line","column","endLine","endColumn"]){if(n[e]!==undefined){const i=n[e];if(typeof i!=="number"||!Number.isInteger(i)||i<1){t.push(`location.${e} must be a positive integer when present`)}}}for(const e of Object.keys(n)){if(!p.has(e))t.push(`unknown location property: ${e}`)}return t}const d=e(import.meta.url)("node:fs");class ConfigParseError extends Error{line;column;rawOffset;constructor(e,t){super(e);this.name="ConfigParseError";this.line=t.line;this.column=t.column;this.rawOffset=t.rawOffset;if(t.cause){this.cause=t.cause}}}function lineColumnOfOffset(e,t){const n=Math.max(0,Math.min(t,e.length));let i=1;let s=1;for(let t=0;t=this.len)break;const e=this.src[this.pos];if(e==="#"){this.skipComment();continue}if(e==="["){if(this.src[this.pos+1]==="["){this.parseArrayOfTablesHeader()}else{this.parseTableHeader()}continue}this.parseKeyValue(this.current)}return this.root}skipWhitespaceAndNewlines(){while(this.pos=this.len)return;const e=this.src[this.pos];if(e==="#"){this.skipComment();return}if(e==="\n"||e==="\r")return;throw new Error(`Unexpected character ${JSON.stringify(e)} at offset ${this.pos}; expected end of line`)}parseTableHeader(){this.pos++;this.skipInlineWhitespace();const e=this.parseKeyChain();this.skipInlineWhitespace();if(this.src[this.pos]!=="]"){throw new Error(`Expected ']' at offset ${this.pos}`)}this.pos++;this.expectLineEnd();const t=e.join(this.PATH_KEY_SEPARATOR);if(this.aotPaths.has(t)){throw new Error(`Cannot redefine array-of-tables [[${e.join(".")}]] as a standard table [${e.join(".")}] at offset ${this.pos}`)}const n=this.descendTablePath(e,true);if(this.definedTables.has(t)){throw new Error(`Duplicate table definition: [${e.join(".")}] at offset ${this.pos}`)}this.definedTables.add(t);this.current=n}parseArrayOfTablesHeader(){this.pos+=2;this.skipInlineWhitespace();const e=this.parseKeyChain();this.skipInlineWhitespace();if(this.src[this.pos]!=="]"||this.src[this.pos+1]!=="]"){throw new Error(`Expected ']]' at offset ${this.pos}`)}this.pos+=2;this.expectLineEnd();const t=this.descendTablePath(e.slice(0,-1),true);const n=e[e.length-1];let i=t[n];if(i===undefined){i=[];t[n]=i;this.aotPaths.add(e.join(this.PATH_KEY_SEPARATOR))}else if(!Array.isArray(i)){throw new Error(`Key ${e.join(".")} is not an array-of-tables`)}const s=e.join(this.PATH_KEY_SEPARATOR)+this.PATH_KEY_SEPARATOR;for(const e of this.definedTables){if(e.startsWith(s)){this.definedTables.delete(e)}}const r={};i.push(r);this.current=r}descendTablePath(e,t){let n=this.root;for(let t=0;te===i[t]));if(!f)continue;const p=i.slice(o.length);if(p.length===0)continue;const d=p.map(escapeForRegex).join("\\s*\\.\\s*");const h=new RegExp(`^\\s*${d}\\s*=`);if(h.test(n))return t;if(p.length===1){const e=p[0];const i=new RegExp(`^\\s*(?:${escapeForRegex(e)}|"${escapeForRegex(e)}"|'${escapeForRegex(e)}')\\s*(?:\\.|=)`);if(i.test(n))return t}}return 0}function updateMultilineStringState(e,t){let n=t;let i=0;while(itrue;const n=lineOfOffset(e,t.start);const i=lineOfOffset(e,Math.max(t.start,t.end-1));return e=>e>=n&&e<=i}function findLineByRegex(e,t,n){const i=stripJsonComments(e);const s=n?i.slice(n.start,n.end):i;const r=t.exec(s);if(!r)return 0;const o=(n?n.start:0)+r.index;return lineOfOffset(e,o)}function lineOfOffset(e,t){let n=1;for(let i=0;i=i)break;const s=e[n];if(s==='"'){n++;const s=n;while(n`${e}=${t}`)).sort();t.push(`env=${n.join("|")}`)}return t.join("\n")}function normalizeExecutable(e){const t=e.trim();const n=t.replace(/\\/g,"/");const i=/\.(cmd|exe|bat|ps1)$/i.test(n);const s=n.replace(/\.(cmd|exe|bat|ps1)$/i,"");const r=i||t.includes("\\");const o=r?s.toLowerCase():s;const a=o.split("/").pop()??o;if(g.has(a.toLowerCase())){return r?a.toLowerCase():a}return o}const g=new Set(["node","npx","npm","pnpm","yarn","python","python3","pip","pip3","pipx","uvx","uv","ruby","gem","bundle","perl","cpan","bash","sh","zsh","fish","powershell","pwsh","deno","bun","tsx","ts-node"]);function normalizePath(e){return e.trim().replace(/\\/g,"/").replace(/\/+$/,"")}const w=new Set(["-y","--yes"]);const y=new Set(["-v","-V","-q","-h","-d","--verbose","--quiet","--silent","--debug","--help","--version","--force","--dry-run","--no-cache","--no-color","--no-progress","--json"]);function canonicalizeArgs(e){const t=e.filter((e=>!w.has(e)));const n=[];const i=[];let s=false;let r=0;for(let e=0;eet?1:0));const o=[...n];for(const[e,t]of i){if(e.startsWith("__pos_")){o.push(e.slice(e.indexOf("__",6)+2))}else if(t===null){o.push(e)}else{o.push(`${e}=${t}`)}}return o}const b={low:1,medium:2,high:3,critical:4};function rankSeverity(e){return b[e]}function passesSeverityThreshold(e,t){return rankSeverity(e)>=rankSeverity(t)}function anyAtOrAbove(e,t){for(const n of e){if(passesSeverityThreshold(n.severity,t))return true}return false}function emitFindingAnnotation(e){const t=e.severity==="critical"||e.severity==="high"?"error":"warning";const n=[];if(e.location?.file)n.push(`file=${escapeProperty(e.location.file)}`);if(e.location?.line!=null)n.push(`line=${e.location.line}`);if(e.location?.column!=null)n.push(`col=${e.location.column}`);if(e.location?.endLine!=null)n.push(`endLine=${e.location.endLine}`);if(e.location?.endColumn!=null)n.push(`endColumn=${e.location.endColumn}`);n.push(`title=${escapeProperty(`[${e.kind}] ${e.severity}`)}`);const i=escapeData(e.message);return`::${t} ${n.join(",")}::${i}`}function escapeData(e){return e.replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function escapeProperty(e){return e.replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A").replace(/:/g,"%3A").replace(/,/g,"%2C")}function generateWorkflowSummary(e,t={}){const n=t.title??"Findings";const i=t.perSeverityLimit??100;const s=t.messageMaxLength??200;if(e.length===0){return`# ${n}\n\nNo findings.\n`}const r={critical:[],high:[],medium:[],low:[]};for(const t of e)r[t.severity].push(t);const o={critical:r.critical.length,high:r.high.length,medium:r.medium.length,low:r.low.length};const a=[];a.push(`# ${n}`,"");a.push(`**Total**: ${e.length} finding${e.length===1?"":"s"} — `+`${o.critical} critical, ${o.high} high, `+`${o.medium} medium, ${o.low} low`);a.push("");const c=["critical","high","medium","low"];for(const e of c){const t=r[e];if(t.length===0)continue;const n=t.slice(0,i);const o=t.length-n.length;a.push(``);a.push(`${t.length} ${e}`);a.push("");a.push("| File | Line | Kind | Message |");a.push("|------|------|------|---------|");for(const e of n){a.push("| "+[escapeMarkdownTableCell(e.location?.file??"—"),e.location?.line??"—",escapeMarkdownTableCell(e.kind),escapeMarkdownTableCell(truncate(e.message,s))].join(" | ")+" |")}if(o>0){a.push(`| _(+${o} more ${e} finding${o===1?"":"s"})_ | | | |`)}a.push("");a.push("");a.push("")}return a.join("\n")}function truncate(e,t){if(e.length<=t)return e;return e.slice(0,Math.max(1,t-1))+"…"}function escapeMarkdownTableCell(e){return String(e).replace(/&/g,"&").replace(//g,">").replace(/\|/g,"\\|").replace(/\r?\n/g," ")}async function readJsonObject(e){return(await discovery_readJsonObjectWithSource(e)).json}async function discovery_readJsonObjectWithSource(e){try{const t=await readFile(e,"utf8");const n=JSON.parse(t);return{json:isRecord(n)?n:{},text:t}}catch(e){if(isNodeError(e)&&e.code==="ENOENT"){return{json:{},text:""}}throw e}}function configPath(e,t){return(0,o.join)(e,t)}function isRecord(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function discovery_lineOfJsonKey(e,t){const n=lineOfJsonKey(e,t);return n===0?undefined:n}function discovery_lineOfJsonStringValue(e,t){const n=lineOfJsonStringValue(e,t);return n===0?undefined:n}function isNodeError(e){return e instanceof Error&&"code"in e}var _=i(924);const k=["postinstall","preinstall","prepare","install"];async function detectPackageScripts(e){const t=e.mode==="directories"?await(0,_.kf)(e.newRoot):await listChangedPackageJsonFiles(e.repo,e.base,e.head);const n=[];for(const i of t){const t=await readScriptsAt(e,i,"old");const s=await readScriptsAt(e,i,"new");const r=await readPackageTextAt(e,i,"new");n.push(...compareScripts(i,t,s,r))}return n}async function listChangedPackageJsonFiles(e,t,n){return(await(0,_.Eh)(e,t,n)).filter(s.xr)}async function readScriptsAt(e,t,n){const i=await readPackageTextAt(e,t,n);if(!i){return{}}try{const e=JSON.parse(i);if(!isRecord(e)||!isRecord(e.scripts)){return{}}const t={};for(const[n,i]of Object.entries(e.scripts)){if(typeof i==="string"){t[n]=i}}return t}catch{return{}}}async function readPackageTextAt(e,t,n){if(e.mode==="directories"){const i=n==="old"?e.oldRoot:e.newRoot;try{return await(0,r.readFile)(configPath(i,t),"utf8")}catch{return""}}const i=n==="old"?e.base:e.head;return await(0,_.p9)(e.repo,i,t)??""}function compareScripts(e,t,n,i){const s=[];for(const r of k){const o=n[r];if(!o){continue}const a=t[r];if(a===o){continue}const c=discovery_lineOfJsonKey(i,r)??discovery_lineOfJsonStringValue(i,o);s.push({kind:"capability_echo.lifecycle_script_added",surface:"package",severity:"high",file:e,line:c,subject:`package.json ${r} script`,message:`Added or changed npm ${r} lifecycle script.`,recommendation:"Review lifecycle scripts carefully; they run automatically on install."});s.push(...analyzeScriptContent(e,r,o,i))}for(const[r,o]of Object.entries(n)){if(k.includes(r)){continue}const n=t[r];if(n===o){continue}s.push(...analyzeScriptContent(e,r,o,i))}return s}function analyzeScriptContent(e,t,n,i){const s=[];const r=discovery_lineOfJsonStringValue(i,n)??discovery_lineOfJsonKey(i,t);if(/(?:curl[^\n|]*\|\s*(?:ba)?sh|wget[^\n|]*\|\s*sh|Invoke-Expression|iex\s*\()/i.test(n)){s.push({kind:"capability_echo.script_pipe_to_shell",surface:"package",severity:"critical",file:e,line:r,subject:`package.json ${t} pipe-to-shell`,message:"Script downloads and pipes content directly into a shell.",recommendation:"Replace remote pipe-to-shell patterns with pinned, reviewable install steps."})}if(/\b(curl|wget|npm publish)\b/i.test(n)||/\bnpx\b(?![^\s]*@\d+\.\d+\.\d+)/i.test(n)){s.push({kind:"capability_echo.script_network_command",surface:"package",severity:"medium",file:e,line:r,subject:`package.json ${t} network command`,message:"Script performs a network or publish command.",recommendation:"Pin package versions and verify remote commands before merge."})}return s}const v=new Set(["puppeteer","puppeteer-core","playwright","playwright-core","cypress","webdriverio","selenium-webdriver","nightwatch","execa","cross-spawn","node-pty","shelljs","zx","tinyspawn","node-fetch","undici","got","axios","request","superagent","vm2","isolated-vm","socks-proxy-agent","https-proxy-agent","ssh2","node-ssh"]);const S=new Set(["@segment/analytics-node","mixpanel","amplitude-js","posthog-js","@sentry/node","@sentry/browser"]);const E=["dependencies","devDependencies","optionalDependencies","peerDependencies"];async function detectPackageDeps(e){const t=e.mode==="directories"?await(0,_.kf)(e.newRoot):await listChangedPackageJsonFiles(e.repo,e.base,e.head);const n=[];for(const i of t){const t=await readPackageTextAt(e,i,"old");const s=await readPackageTextAt(e,i,"new");n.push(...compareDeps(i,t,s))}return n}function compareDeps(e,t,n){const i=readAllDeps(t);const s=readAllDeps(n);const r=[];for(const[t,o]of s.entries()){if(i.has(t)){continue}if(v.has(t)){r.push({kind:"capability_echo.high_capability_dep_added",surface:"package",severity:"high",file:e,line:discovery_lineOfJsonStringValue(n,o)??discovery_lineOfJsonKey(n,t),subject:t,message:`Added dependency "${t}" can reach the network, spawn subprocesses, or evaluate code.`,recommendation:"Confirm this dependency is required for the stated change and that its usage is scoped."});continue}if(S.has(t)){r.push({kind:"capability_echo.telemetry_dep_added",surface:"package",severity:"medium",file:e,line:discovery_lineOfJsonStringValue(n,o)??discovery_lineOfJsonKey(n,t),subject:t,message:`Added telemetry/analytics dependency "${t}" — ships an outbound network surface by default.`,recommendation:"Verify the telemetry destination, payload, and opt-out posture."})}}return r}function readAllDeps(e){const t=new Map;if(!e.trim()){return t}let n;try{n=JSON.parse(e)}catch{return t}if(!isRecord(n)){return t}for(const e of E){const i=n[e];if(!isRecord(i)){continue}for(const[e,n]of Object.entries(i)){if(typeof n==="string"){t.set(e,n)}}}return t}function detectPyCapability(e,t={}){const n=[];const i=py_capability_collectSecretVariables(e,t);for(const t of e){if(!(0,s.UD)(t.file)||(0,s.w5)(t.content)){continue}const e=(0,s.hl)(t.file);n.push(...detectPyNetwork(t,e));n.push(...detectPySecretExfil(t,e,i.get(t.file)??new Set));n.push(...detectPySubprocess(t,e));n.push(...detectPyDynamicExec(t,e));n.push(...detectPyUnsafeDeserialize(t,e))}return n}function py_capability_collectSecretVariables(e,t){const n=new Map;for(const t of e){if(!(0,s.UD)(t.file)){continue}py_capability_addSecretVariable(n,t.file,t.content)}for(const[e,i]of Object.entries(t)){if(!(0,s.UD)(e)){continue}for(const t of i.split(/\r?\n/)){py_capability_addSecretVariable(n,e,t)}}return n}function py_capability_addSecretVariable(e,t,n){const i=n.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:os\.environ\s*(?:\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\]|\.get\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"])|os\.getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"])/i);if(!i){return}const s=e.get(t)??new Set;s.add(i[1]);e.set(t,s)}function detectPyNetwork(e,t){const n=/\b(?:requests|httpx)\.(?:get|post|put|delete|patch|head|options|request)\s*\(|\burllib(?:2)?\.(?:request\.)?urlopen\s*\(|\burlopen\s*\(|\burllib\.request\.urlretrieve\s*\(|\baiohttp\.ClientSession\s*\(/i;if(!n.test(e.content)){return[]}if(!/(?:https?:\/\/|['"]https?:\/\/)/i.test(e.content)){return[]}return[{kind:"capability_echo.external_fetch_added",surface:"source",severity:t?"low":"medium",file:e.file,line:e.line,subject:"External network call (Python)",message:"Added Python performs an external HTTP request that expands network reach.",recommendation:"Review the endpoint, request payload, and whether the call belongs in this change."}]}function detectPySecretExfil(e,t,n){if(!isPyExternalRequest(e.content)||!referencesPyEnvSecret(e.content)&&!py_capability_referencesSecretVariable(e.content,n)){return[]}return[{kind:"capability_echo.source_secret_exfil_pattern",surface:"source",severity:t?"medium":"high",file:e.file,line:e.line,subject:"Source secret exfiltration pattern (Python)",message:"Added Python sends environment-secret-shaped data to an external endpoint.",recommendation:"Do not send env secrets to external services unless the endpoint and payload are explicitly required."}]}function isPyExternalRequest(e){return/\b(?:requests|httpx)\.(?:get|post|put|delete|patch|head|options|request)\s*\(|\burllib(?:2)?\.(?:request\.)?urlopen\s*\(|\burlopen\s*\(|\burllib\.request\.urlretrieve\s*\(|\baiohttp\.ClientSession\s*\(/i.test(e)&&/(?:https?:\/\/|['"]https?:\/\/)/i.test(e)}function referencesPyEnvSecret(e){return/\bos\.environ\s*(?:\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\]|\.get\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"])/i.test(e)||/\bos\.getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]/i.test(e)}function py_capability_referencesSecretVariable(e,t){return[...t].some((t=>new RegExp(String.raw`\b${py_capability_escapeRegExp(t)}\b`).test(e)))}function py_capability_escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function detectPySubprocess(e,t){const n=/\bsubprocess\.(?:run|call|Popen|check_call|check_output|getoutput|getstatusoutput)\s*\(|\bos\.(?:system|popen|execv\w*|spawnv?\w*)\s*\(|\bcommands\.getoutput\s*\(|\bpty\.spawn\s*\(/i;if(!n.test(e.content)){return[]}return[{kind:"capability_echo.subprocess_spawn_added",surface:"source",severity:t?"low":"high",file:e.file,line:e.line,subject:"Subprocess spawn (Python)",message:"Added Python can spawn shell commands or subprocesses.",recommendation:"Confirm the command source is trusted and scoped to the task."}]}function detectPyDynamicExec(e,t){const n=/\beval\s*\(|\bexec\s*\(|\bcompile\s*\(|\b__import__\s*\(|\bimportlib\.import_module\s*\(/i;if(!n.test(e.content)){return[]}return[{kind:"capability_echo.dynamic_eval_added",surface:"source",severity:t?"medium":"critical",file:e.file,line:e.line,subject:"Dynamic code execution (Python)",message:"Added Python can evaluate dynamic code or import modules by name at runtime.",recommendation:"Avoid eval-style execution unless strictly required; never feed user input to these."}]}function detectPyUnsafeDeserialize(e,t){const n=/\bpickle\.(?:load|loads)\s*\(|\bmarshal\.(?:load|loads)\s*\(|\byaml\.load\s*\((?![^)]*Loader\s*=\s*(?:yaml\.)?SafeLoader)/i;if(!n.test(e.content)){return[]}return[{kind:"capability_echo.unsafe_deserialize_added",surface:"source",severity:t?"medium":"critical",file:e.file,line:e.line,subject:"Unsafe deserialization (Python)",message:"Added Python deserializes untrusted-shaped input (pickle / marshal / yaml.load).",recommendation:"Use yaml.safe_load and avoid pickle/marshal on data crossing trust boundaries."}]}function detectShellCapability(e){const t=[];for(const n of e){if(!(0,s.Z7)(n.file)||(0,s.w5)(n.content)){continue}t.push(...shell_capability_detectPipeToShell(n));t.push(...detectExternalDownload(n))}return t}function shell_capability_detectPipeToShell(e){if(!/(?:curl|wget|Invoke-WebRequest|iwr)[^\n|]*https?:\/\/[^\n|]*\|\s*(?:ba)?sh\b|iex\s*\(|Invoke-Expression/i.test(e.content)){return[]}return[{kind:"capability_echo.shell_pipe_to_shell",surface:"source",severity:"critical",file:e.file,line:e.line,subject:"Shell remote pipe-to-shell",message:"Added shell script downloads remote content and pipes it directly to a shell.",recommendation:"Replace remote pipe-to-shell with pinned, reviewable install steps."}]}function detectExternalDownload(e){if(!/\b(curl|wget|Invoke-WebRequest|iwr)\b[^\n]*https?:\/\//i.test(e.content)){return[]}return[{kind:"capability_echo.shell_external_download",surface:"source",severity:"medium",file:e.file,line:e.line,subject:"Shell external download",message:"Added shell script downloads content from an external URL.",recommendation:"Verify the URL, checksum or signature, and whether the download belongs in this change."}]}const x=/^\s*(?:actions|artifact-metadata|attestations|checks|code-quality|contents|deployments|discussions|id-token|issues|packages|pages|pull-requests|security-events|statuses)\s*:\s*write\b/i;function detectWorkflowPermissions(e,t={}){const n=[];const i=new Set(e.filter((e=>(0,s.Kr)(e.file)&&isPullRequestTargetLine(e.content))).map((e=>e.file)));const r=collectSecretEnvVars(e,t);for(const[e,n]of Object.entries(t)){if((0,s.Kr)(e)&&hasPullRequestTargetWorkflow(n)){i.add(e)}}for(const t of e){if(!(0,s.Kr)(t.file)){continue}n.push(...detectPullRequestTarget(t));n.push(...detectPullRequestHeadCheckoutOnTarget(t,i.has(t.file)));n.push(...detectSelfHostedRunner(t));n.push(...detectMutableActionRef(t));n.push(...detectWritePermissions(t));n.push(...detectExternalCurl(t));n.push(...detectSecretsInherit(t));n.push(...workflow_permissions_detectSecretExfil(t,r.get(t.file)??new Set));n.push(...detectDockerHostControl(t))}return n}function collectSecretEnvVars(e,t){const n=new Map;for(const t of e){if(!(0,s.Kr)(t.file)){continue}addSecretEnvVar(n,t.file,t.content)}for(const[e,i]of Object.entries(t)){if(!(0,s.Kr)(e)){continue}for(const t of i.split(/\r?\n/)){addSecretEnvVar(n,e,t)}}return n}function addSecretEnvVar(e,t,n){const i=n.match(/^\s*([A-Z_][A-Z0-9_]*)\s*:\s*.*\$\{\{\s*secrets\./i);if(!i){return}const s=e.get(t)??new Set;s.add(i[1]);e.set(t,s)}function detectWritePermissions(e){const t=e.content;if(!/permissions\s*:/i.test(t)&&!x.test(t)){return[]}if(x.test(t)){return[{kind:"capability_echo.workflow_permission_write",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions write permission",message:"Workflow grants repository or package write permissions.",recommendation:"Use the narrowest permission scope required for this job."}]}if(/^\s*permissions\s*:\s*(?:write|write-all|admin)\b/i.test(t)){return[{kind:"capability_echo.workflow_permission_write",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions broad write permission",message:"Workflow grants broad write or admin permissions.",recommendation:"Prefer explicit per-resource permissions instead of top-level write/admin."}]}return[]}function detectPullRequestTarget(e){if(!isPullRequestTargetLine(e.content)){return[]}return[{kind:"capability_echo.workflow_pull_request_target",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions pull_request_target trigger",message:"Workflow runs on pull_request_target, which can expose elevated token or secret context to PR-triggered automation.",recommendation:"Use pull_request unless elevated base-repository context is required; never run untrusted PR code with pull_request_target privileges."}]}function detectPullRequestHeadCheckoutOnTarget(e,t){if(!t||!isPullRequestHeadCheckoutLine(e.content)){return[]}return[{kind:"capability_echo.workflow_pr_head_checkout_on_target",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions PR-head checkout under pull_request_target",message:"Workflow checks out pull request head code in a pull_request_target workflow.",recommendation:"Use pull_request for untrusted PR code, or avoid checking out PR head code under pull_request_target."}]}function isPullRequestTargetLine(e){return/^\s*pull_request_target\s*:/i.test(e)}function hasPullRequestTargetWorkflow(e){return e.split(/\r?\n/).some(isPullRequestTargetLine)}function isPullRequestHeadCheckoutLine(e){return/^\s*(?:ref|repository)\s*:\s*.*github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)/i.test(e)}function detectSelfHostedRunner(e){if(!/^\s*runs-on\s*:\s*(?:.*\bself-hosted\b|.*\[\s*self-hosted\b)/i.test(e.content)&&!/^\s*-\s*self-hosted\s*(?:#.*)?$/i.test(e.content)){return[]}return[{kind:"capability_echo.workflow_self_hosted_runner",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions self-hosted runner",message:"Workflow runs on a self-hosted runner, which can expand PR-triggered automation into private infrastructure.",recommendation:"Use GitHub-hosted runners for untrusted PR code, or isolate self-hosted runners with strict labels, permissions, and cleanup."}]}function detectMutableActionRef(e){const t=extractWorkflowUsesRef(e.content);if(!t||isLocalActionRef(t)||/^docker:\/\//i.test(t)){return[]}const n=t.lastIndexOf("@");if(n===-1){return[]}const i=t.slice(n+1);if(!isMutableActionVersionRef(i)){return[]}return[{kind:"capability_echo.workflow_mutable_action_ref",surface:"workflow",severity:"medium",file:e.file,line:e.line,subject:"GitHub Actions mutable action reference",message:"Workflow uses a mutable remote action reference.",recommendation:"Pin third-party actions to a reviewed commit SHA before merge."}]}function extractWorkflowUsesRef(e){return e.match(/^\s*(?:-\s*)?uses\s*:\s*['"]?([^'"\s#]+)['"]?/i)?.[1]}function isLocalActionRef(e){return e.startsWith("./")||e.startsWith("../")||e.startsWith("/")}function isMutableActionVersionRef(e){return/^(main|master|trunk|develop|dev|latest|head)$/i.test(e)}function detectExternalCurl(e){if(!/\b(curl|wget|Invoke-WebRequest|fetch\s*\()/i.test(e.content)){return[]}if(!referencesExternalUrlOrVariable(e.content)){return[]}return[{kind:"capability_echo.workflow_external_curl",surface:"workflow",severity:"medium",file:e.file,line:e.line,subject:"Workflow external request",message:"Workflow step performs an external network request.",recommendation:"Verify the URL, payload, and whether the request is necessary in CI."}]}function referencesExternalUrlOrVariable(e){const t=e.match(/https?:\/\/[^\s'"`)]+/gi)??[];for(const e of t){if(!isLocalUrl(e)){return true}}if(t.length===0&&/\$\{?\w|\$\{\{/.test(e)){return true}return false}function isLocalUrl(e){return/^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:[/?#]|$)/i.test(e)}function detectSecretsInherit(e){if(!/^\s*secrets\s*:\s*inherit\s*(?:#.*)?$/i.test(e.content)){return[]}return[{kind:"capability_echo.workflow_secrets_inherit",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"GitHub Actions inherited secrets",message:"Workflow passes all caller secrets to a reusable workflow.",recommendation:"Pass only explicit secrets required by the reusable workflow."}]}function workflow_permissions_detectSecretExfil(e,t){const n=e.content;const i=/\$\{\{\s*secrets\.|\$\{?\s*secrets\.|env\.[A-Z0-9_]+/i.test(n)||[...t].some((e=>referencesShellVariable(n,e)));const s=/\b(curl|wget|Invoke-WebRequest|fetch\s*\()/i.test(n);const r=/\|\s*(bash|sh|powershell|pwsh)/i.test(n);if(!i||!s){return[]}return[{kind:"capability_echo.workflow_secret_exfil_pattern",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"Workflow secret exfiltration pattern",message:"Workflow step references secrets or env values alongside an external request or shell pipe.",recommendation:"Review whether secrets could leave the runner through this step."}]}function referencesShellVariable(e,t){const n=workflow_permissions_escapeRegExp(t);return new RegExp(String.raw`(?:\$\{${n}\}|\$${n}\b|%${n}%)`).test(e)}function workflow_permissions_escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function detectDockerHostControl(e){const t=[];const n=e.content;if(/\/var\/run\/docker\.sock(?::\/var\/run\/docker\.sock)?/i.test(n)){t.push({kind:"capability_echo.workflow_docker_socket_mount",surface:"workflow",severity:"critical",file:e.file,line:e.line,subject:"Workflow Docker socket mount",message:"Workflow mounts the host Docker socket, which can grant control over the runner host.",recommendation:"Avoid Docker socket mounts in CI unless the job is isolated and the image/commands are trusted."})}if(/\bdocker\s+run\b.*\s--privileged(?:\s|$)/i.test(n)){t.push({kind:"capability_echo.workflow_privileged_container",surface:"workflow",severity:"high",file:e.file,line:e.line,subject:"Workflow privileged container",message:"Workflow runs a privileged container, expanding kernel and device-level access in CI.",recommendation:"Use the narrowest container privileges required, and avoid privileged mode for agent-run code."})}return t}var $=i(665);async function runCapabilityDiff(e){const t=e.mode==="directories"?await(0,_.BH)(e.oldRoot,e.newRoot):await(0,_._u)(e.repo,e.base,e.head);const n=e.mode==="directories"?{mode:"directories",oldRoot:e.oldRoot,newRoot:e.newRoot}:{mode:"git",repo:e.repo,base:e.base,head:e.head};const[i,s]=await Promise.all([detectPackageScripts(n),detectPackageDeps(n)]);const r=[...detectWorkflowPermissions(t.addedLines,t.newFileContents),...detectDockerfileCapability(t.addedLines),...detectJsCapability(t.addedLines,t.newFileContents),...detectPyCapability(t.addedLines,t.newFileContents),...detectShellCapability(t.addedLines),...i,...s];return(0,$.LU)(r,t)}},924:(t,n,i)=>{i.d(n,{rd:()=>GitDiffSetupError,BH:()=>collectDirectoryDiff,_u:()=>collectGitDiff,Eh:()=>listGitChangedFiles,kf:()=>listPackageJsonFiles,p9:()=>readFileAtGitRef});const s=e(import.meta.url)("node:child_process");var r=i(455);var o=i(760);const a=e(import.meta.url)("node:util");var c=i(431);const l=(0,a.promisify)(s.execFile);const u=["source","package","workflow","container"];class GitDiffSetupError extends Error{base;head;constructor(e,t,n){super(e);this.base=t;this.head=n;this.name="GitDiffSetupError"}}async function collectDirectoryDiff(e,t){const n=await listScannableFiles(t);const i=[];const s=new Set;const a={};for(const c of n){const n=(0,o.join)(e,c);const l=(0,o.join)(t,c);const u=await(0,r.readFile)(l,"utf8");const f=await runGitNoIndexDiff(n,l);if(f.trim()){s.add(c);a[c]=u;i.push(...parseUnifiedDiff(f,c));continue}let p="";try{p=await(0,r.readFile)(n,"utf8")}catch{p=""}if(p===u){continue}s.add(c);a[c]=u;if(!p){i.push(...allLinesAsAdded(c,u))}}return{addedLines:i,changedFileCount:s.size,scannedSurfaces:surfacesForFiles([...s]),newFileContents:a}}async function collectGitDiff(e,t,n){const i=await gitRefExists(e,t);const s=await gitRefExists(e,n);if(!i||!s){throw new GitDiffSetupError(`CapabilityEcho could not compare base '${t}' and head '${n}'.`,t,n)}const r=await listGitChangedFiles(e,t,n);const o=r.filter(c.Iy);if(o.length===0){return{addedLines:[],changedFileCount:0,scannedSurfaces:[],newFileContents:{}}}const{stdout:a}=await l("git",["-C",e,"diff","-U0",`${t}..${n}`,"--",...o],{encoding:"utf8",maxBuffer:20*1024*1024});const u=await readChangedFilesAtRef(e,n,o);return{addedLines:parseUnifiedDiff(a).map((e=>({...e,file:normalizeGitDiffPath(e.file)}))),changedFileCount:o.length,scannedSurfaces:surfacesForFiles(o),newFileContents:u}}function parseUnifiedDiff(e,t){const n=[];let i="";let s=0;for(const r of e.split(/\r?\n/)){if(r.startsWith("+++ ")){const e=r.slice(4).trim();if(e.startsWith("b/")){i=e.slice(2)}else if(e==="/dev/null"){i=""}else{i=e}continue}if(r.startsWith("@@")){const e=r.match(/\+(\d+)(?:,(\d+))?/);s=e?Number.parseInt(e[1],10):0;continue}if(!i||i==="/dev/null"){continue}if(r.startsWith("+")&&!r.startsWith("+++")){n.push({file:(t??normalizeGitDiffPath(i)).replace(/\\/g,"/"),line:s,content:r.slice(1)});s+=1;continue}if(r.startsWith("-")&&!r.startsWith("---")){continue}if(r.startsWith(" ")||r.startsWith("\\")){s+=1}}return n}async function listScannableFiles(e,t=""){const n=await(0,r.readdir)((0,o.join)(e,t),{withFileTypes:true});const i=[];for(const s of n){if(s.name==="node_modules"||s.name===".git"){continue}const n=t?`${t}/${s.name}`:s.name;if(s.isDirectory()){i.push(...await listScannableFiles(e,n));continue}if((0,c.Iy)(n)){i.push(n.replace(/\\/g,"/"))}}return i}async function listGitChangedFiles(e,t,n){const{stdout:i}=await l("git",["-C",e,"diff","--name-only",`${t}..${n}`],{encoding:"utf8",maxBuffer:10*1024*1024});return i.split(/\r?\n/).map((e=>e.trim())).filter(Boolean).map((e=>e.replace(/\\/g,"/")))}async function runGitNoIndexDiff(e,t){try{const{stdout:n}=await l("git",["diff","--no-index","-U0",e,t],{encoding:"utf8",maxBuffer:10*1024*1024});return n}catch(e){if(isExecError(e)&&typeof e.stdout==="string"){return e.stdout}return""}}function allLinesAsAdded(e,t){const n=t.split(/\r?\n/);return n.map(((t,n)=>({file:e.replace(/\\/g,"/"),line:n+1,content:t})))}async function gitRefExists(e,t){try{await l("git",["-C",e,"rev-parse","--verify",`${t}^{commit}`]);return true}catch(e){if(isExecError(e)){return false}throw e}}function surfacesForFiles(e){const t=new Set;for(const n of e){const e=(0,c.qk)(n);if(e){t.add(e)}}return u.filter((e=>t.has(e)))}function isExecError(e){return e instanceof Error&&"code"in e}function normalizeGitDiffPath(e){return e.replace(/\\/g,"/").replace(/^[a-z]:\//i,"").replace(/^b\//,"")}async function readFileAtGitRef(e,t,n){try{const{stdout:i}=await l("git",["-C",e,"show",`${t}:${n}`],{encoding:"utf8",maxBuffer:10*1024*1024});return i}catch(e){if(isExecError(e)){return null}throw e}}async function readChangedFilesAtRef(e,t,n){const i=await Promise.all(n.map((async n=>{const i=await readFileAtGitRef(e,t,n);return i===null?undefined:[n,i]})));return Object.fromEntries(i.filter((e=>e!==undefined)))}async function listPackageJsonFiles(e,t=""){const n=await(0,r.readdir)((0,o.join)(e,t),{withFileTypes:true});const i=[];for(const s of n){if(s.name==="node_modules"||s.name===".git"){continue}const n=t?`${t}/${s.name}`:s.name;if(s.isDirectory()){i.push(...await listPackageJsonFiles(e,n));continue}if(s.name==="package.json"){i.push(n.replace(/\\/g,"/"))}}return i}function relativeFromRoots(e,t){return relative(e,t).replace(/\\/g,"/")}},431:(e,t,n)=>{n.d(t,{Iy:()=>isScannable,Kr:()=>isWorkflowFile,Mn:()=>isJsFile,UD:()=>isPyFile,Z7:()=>isShellFile,hl:()=>isTestFile,pZ:()=>isDockerfile,qk:()=>surfaceForPath,w5:()=>isCommentLine,xr:()=>isPackageJsonFile});const i=new Set([".mcp.json",".cursor/mcp.json",".vscode/mcp.json",".codeium/windsurf/mcp_config.json",".claude/settings.json",".codex/config.toml","AGENTS.md"]);function normalizeRelativePath(e){return e.replace(/\\/g,"/")}function isExcluded(e){const t=normalizeRelativePath(e);if(i.has(t)){return true}return t.startsWith(".cursor/rules/")}function isScannable(e){return surfaceForPath(e)!==undefined}function surfaceForPath(e){const t=normalizeRelativePath(e);if(isExcluded(t)){return undefined}if(t==="package.json"||t.endsWith("/package.json")){return"package"}if(t.startsWith(".github/workflows/")&&/\.(ya?ml)$/i.test(t)){return"workflow"}if(isDockerfile(t)){return"container"}if(isJsFile(t)||isPyFile(t)||isShellFile(t)){return"source"}return undefined}function isTestFile(e){const t=normalizeRelativePath(e);if(t.includes("__tests__/")||t.includes("/tests/")){return true}if(/(^|\/)test_[^/]+\.py$/i.test(t)||/_test\.py$/i.test(t)){return true}return/\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(t)}function isCommentLine(e){const t=e.trim();return t.startsWith("//")||t.startsWith("/*")||t.startsWith("*")||t.startsWith("*/")||t.startsWith("#")}function isWorkflowFile(e){const t=normalizeRelativePath(e);return t.startsWith(".github/workflows/")&&/\.(ya?ml)$/i.test(t)}function isPackageJsonFile(e){const t=normalizeRelativePath(e);return t==="package.json"||t.endsWith("/package.json")}function isJsFile(e){const t=normalizeRelativePath(e);return/\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(t)}function isPyFile(e){const t=normalizeRelativePath(e);return/\.(py|pyw)$/i.test(t)}function isShellFile(e){const t=normalizeRelativePath(e);return/\.(sh|bash|zsh|ps1|psm1)$/i.test(t)}function isDockerfile(e){const t=normalizeRelativePath(e);const n=t.split("/").pop()??t;return/^Dockerfile(?:\..+)?$/i.test(n)}},665:(e,t,n)=>{n.d(t,{B1:()=>renderReport,LU:()=>createReport,qs:()=>o});const i=["AI-agent config"];const s=["critical","high","medium","low"];const r={source:"source code",package:"package manifests",workflow:"GitHub workflows",container:"container builds"};const o={none:0,low:1,medium:2,high:3,critical:4};const a={"capability_echo.external_fetch_added":"external network fetch calls","capability_echo.source_secret_exfil_pattern":"source secret exfiltration patterns","capability_echo.subprocess_spawn_added":"subprocess or shell spawn calls","capability_echo.dynamic_eval_added":"dynamic code execution","capability_echo.shell_pipe_to_shell":"shell pipe-to-shell downloads","capability_echo.shell_external_download":"shell external downloads","capability_echo.dockerfile_remote_add":"Dockerfile remote ADD instructions","capability_echo.dockerfile_pipe_to_shell":"Dockerfile pipe-to-shell builds","capability_echo.workflow_permission_write":"GitHub Actions write permissions","capability_echo.workflow_pull_request_target":"GitHub Actions pull_request_target triggers","capability_echo.workflow_pr_head_checkout_on_target":"GitHub Actions PR-head checkout under pull_request_target","capability_echo.workflow_self_hosted_runner":"GitHub Actions self-hosted runners","capability_echo.workflow_mutable_action_ref":"GitHub Actions mutable action references","capability_echo.workflow_secrets_inherit":"GitHub Actions inherited secrets","capability_echo.workflow_external_curl":"workflow external network requests","capability_echo.workflow_secret_exfil_pattern":"workflow secret exfiltration patterns","capability_echo.workflow_docker_socket_mount":"workflow Docker socket mounts","capability_echo.workflow_privileged_container":"workflow privileged containers","capability_echo.lifecycle_script_added":"npm lifecycle scripts","capability_echo.script_pipe_to_shell":"pipe-to-shell install scripts","capability_echo.script_network_command":"network or publish npm scripts","capability_echo.high_capability_dep_added":"high-capability dependency additions","capability_echo.telemetry_dep_added":"telemetry dependency additions","capability_echo.unsafe_deserialize_added":"unsafe deserialization"};function createReport(e,t){return{rating:rateFindings(e),findingCount:e.length,changedFileCount:t.changedFileCount,scannedSurfaces:t.scannedSurfaces,excludedSurfaces:[...i],surfaceSummary:buildSurfaceSummary(e),severitySummary:buildSeveritySummary(e),capabilitySummary:buildCapabilitySummary(e),topRecommendations:buildTopRecommendations(e),findings:e}}function renderReport(e,t){if(t==="json"){return`${JSON.stringify(e,null,2)}\n`}if(t==="markdown"){return renderMarkdown(e)}if(t==="github"){return renderGithubAnnotations(e)}return renderText(e)}function buildCapabilitySummary(e){const t=new Set;for(const n of e){t.add(a[n.kind]??n.kind)}return[...t]}function buildTopRecommendations(e){const t=new Set;const n=e.map(((e,t)=>({finding:e,index:t}))).sort(((e,t)=>{const n=o[t.finding.severity]-o[e.finding.severity];return n===0?e.index-t.index:n}));for(const{finding:e}of n){t.add(e.recommendation);if(t.size===3){break}}return[...t]}function rateFindings(e){let t="none";for(const n of e){if(o[n.severity]>o[t]){t=n.severity}}return t}function renderMarkdown(e){const t=[`# CapabilityEcho capability drift: ${e.rating.toUpperCase()}`,""];t.push(`Scanned executable surfaces: ${formatSurfaces(e.scannedSurfaces)}.`);t.push(`Excluded surfaces: ${e.excludedSurfaces.join(", ")}.`,"");if(e.findings.length===0){t.push("No code or workflow capability drift findings.");return`${t.join("\n")}\n`}t.push(`This diff scanned ${e.changedFileCount} changed file${e.changedFileCount===1?"":"s"}.`);t.push(`CapabilityEcho found ${e.findingCount} finding${e.findingCount===1?"":"s"}.`,"");if(e.topRecommendations.length>0){t.push("## Top recommendations","");for(const n of e.topRecommendations){t.push(`- ${n}`)}t.push("")}t.push("## Review summary","");t.push("| Surface | Findings |");t.push("| --- | ---: |");for(const n of["source","package","workflow","container"]){t.push(`| ${r[n]} | ${e.surfaceSummary[n]} |`)}t.push("");t.push("| Severity | Findings |");t.push("| --- | ---: |");for(const n of s){t.push(`| ${capitalize(n)} | ${e.severitySummary[n]} |`)}t.push("");if(e.capabilitySummary.length>0){t.push("## Capability summary","");for(const n of e.capabilitySummary){t.push(`- ${n}`)}t.push("")}for(const n of s){const i=e.findings.filter((e=>e.severity===n));if(i.length===0){continue}t.push(`## ${capitalize(n)}`,"");for(const e of i){t.push(`- **${e.subject}** [${r[e.surface]}] (${e.file}): ${e.message}`);t.push(` Recommendation: ${e.recommendation}`)}t.push("")}return`${t.join("\n").trimEnd()}\n`}function renderText(e){const t=[`CapabilityEcho capability drift: ${e.rating.toUpperCase()}`];t.push(`Scanned executable surfaces: ${formatSurfaces(e.scannedSurfaces)}.`);t.push(`Excluded surfaces: ${e.excludedSurfaces.join(", ")}.`);if(e.capabilitySummary.length>0){t.push(`Signals: ${e.capabilitySummary.join(", ")}`)}if(e.topRecommendations.length>0){t.push(`Top recommendations: ${e.topRecommendations.join(" | ")}`)}for(const n of e.findings){t.push(`[${n.severity.toUpperCase()}] ${n.subject} (${r[n.surface]}): ${n.message}`)}if(e.findings.length===0){t.push("No code or workflow capability drift findings.")}return`${t.join("\n")}\n`}function renderGithubAnnotations(e){if(e.findings.length===0){return""}return e.findings.map((e=>{const t=`CapabilityEcho ${e.severity} ${r[e.surface]} capability drift`;const n=`${e.message} Recommendation: ${e.recommendation}`;const i=[`file=${escapeProperty(e.file)}`];if(e.line&&e.line>0){i.push(`line=${e.line}`)}i.push(`title=${escapeProperty(t)}`);return`::warning ${i.join(",")}::${escapeMessage(n)}`})).join("\n")+"\n"}function escapeMessage(e){return e.replaceAll("%","%25").replaceAll("\r","%0D").replaceAll("\n","%0A")}function escapeProperty(e){return escapeMessage(e).replaceAll(":","%3A").replaceAll(",","%2C")}function capitalize(e){return`${e.slice(0,1).toUpperCase()}${e.slice(1)}`}function buildSurfaceSummary(e){return{source:e.filter((e=>e.surface==="source")).length,package:e.filter((e=>e.surface==="package")).length,workflow:e.filter((e=>e.surface==="workflow")).length,container:e.filter((e=>e.surface==="container")).length}}function buildSeveritySummary(e){return{critical:e.filter((e=>e.severity==="critical")).length,high:e.filter((e=>e.severity==="high")).length,medium:e.filter((e=>e.severity==="medium")).length,low:e.filter((e=>e.severity==="low")).length}}function formatSurfaces(e){if(e.length===0){return"none"}return e.map((e=>r[e])).join(", ")}},455:t=>{t.exports=e(import.meta.url)("node:fs/promises")},760:t=>{t.exports=e(import.meta.url)("node:path")}};var n={};function __nccwpck_require__(e){var i=n[e];if(i!==undefined){return i.exports}var s=n[e]={exports:{}};var r=true;try{t[e](s,s.exports,__nccwpck_require__);r=false}finally{if(r)delete n[e]}return s.exports}(()=>{var e=typeof Symbol==="function"?Symbol("webpack queues"):"__webpack_queues__";var t=typeof Symbol==="function"?Symbol("webpack exports"):"__webpack_exports__";var n=typeof Symbol==="function"?Symbol("webpack error"):"__webpack_error__";var resolveQueue=e=>{if(e&&e.d<1){e.d=1;e.forEach((e=>e.r--));e.forEach((e=>e.r--?e.r++:e()))}};var wrapDeps=i=>i.map((i=>{if(i!==null&&typeof i==="object"){if(i[e])return i;if(i.then){var s=[];s.d=0;i.then((e=>{r[t]=e;resolveQueue(s)}),(e=>{r[n]=e;resolveQueue(s)}));var r={};r[e]=e=>e(s);return r}}var o={};o[e]=e=>{};o[t]=i;return o}));__nccwpck_require__.a=(i,s,r)=>{var o;r&&((o=[]).d=-1);var a=new Set;var c=i.exports;var l;var u;var f;var p=new Promise(((e,t)=>{f=t;u=e}));p[t]=c;p[e]=e=>(o&&e(o),a.forEach(e),p["catch"]((e=>{})));i.exports=p;s((i=>{l=wrapDeps(i);var s;var getResult=()=>l.map((e=>{if(e[n])throw e[n];return e[t]}));var r=new Promise((t=>{s=()=>t(getResult);s.r=0;var fnQueue=e=>e!==o&&!a.has(e)&&(a.add(e),e&&!e.d&&(s.r++,e.push(s)));l.map((t=>t[e](fnQueue)))}));return s.r?r:getResult()}),(e=>(e?f(p[n]=e):u(c),resolveQueue(o))));o&&o.d<0&&(o.d=0)}})();(()=>{__nccwpck_require__.n=e=>{var t=e&&e.__esModule?()=>e["default"]:()=>e;__nccwpck_require__.d(t,{a:t});return t}})();(()=>{__nccwpck_require__.d=(e,t)=>{for(var n in t){if(__nccwpck_require__.o(t,n)&&!__nccwpck_require__.o(e,n)){Object.defineProperty(e,n,{enumerable:true,get:t[n]})}}}})();(()=>{__nccwpck_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})();if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var i=__nccwpck_require__(929);i=await i;var s=i.g;export{s as mainAction}; \ No newline at end of file diff --git a/dist/action.js b/dist/action.js index 6ce8cfb..5fe1266 100644 --- a/dist/action.js +++ b/dist/action.js @@ -1,14 +1,7 @@ import { appendFile, readFile } from 'node:fs/promises'; import { runCapabilityDiff } from './diff.js'; import { GitDiffSetupError } from './git-diff.js'; -import { renderReport } from './report.js'; -const severityRank = { - none: 0, - low: 1, - medium: 2, - high: 3, - critical: 4 -}; +import { renderReport, severityRank } from './report.js'; export async function mainAction(env = process.env) { const repo = getInput(env, 'repo') || env.GITHUB_WORKSPACE || process.cwd(); const event = await readEvent(env); diff --git a/dist/detectors/workflow-permissions.js b/dist/detectors/workflow-permissions.js index 5dd4039..3436ad4 100644 --- a/dist/detectors/workflow-permissions.js +++ b/dist/detectors/workflow-permissions.js @@ -187,6 +187,9 @@ function detectExternalCurl(added) { if (!/\b(curl|wget|Invoke-WebRequest|fetch\s*\()/i.test(added.content)) { return []; } + if (!referencesExternalUrlOrVariable(added.content)) { + return []; + } return [ { kind: 'capability_echo.workflow_external_curl', @@ -200,6 +203,21 @@ function detectExternalCurl(added) { } ]; } +function referencesExternalUrlOrVariable(content) { + const urls = content.match(/https?:\/\/[^\s'"`)]+/gi) ?? []; + for (const url of urls) { + if (!isLocalUrl(url)) { + return true; + } + } + if (urls.length === 0 && /\$\{?\w|\$\{\{/.test(content)) { + return true; + } + return false; +} +function isLocalUrl(url) { + return /^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:[/?#]|$)/i.test(url); +} function detectSecretsInherit(added) { if (!/^\s*secrets\s*:\s*inherit\s*(?:#.*)?$/i.test(added.content)) { return []; diff --git a/dist/git-diff.js b/dist/git-diff.js index 92066fc..f652261 100644 --- a/dist/git-diff.js +++ b/dist/git-diff.js @@ -196,18 +196,10 @@ function isExecError(error) { return error instanceof Error && 'code' in error; } function normalizeGitDiffPath(file) { - const normalized = file.replace(/\\/g, '/'); - const markers = ['/src/', '/.github/workflows/', '/package.json']; - for (const marker of markers) { - const index = normalized.lastIndexOf(marker); - if (index >= 0) { - return normalized.slice(index + 1); - } - } - if (normalized.endsWith('package.json')) { - return 'package.json'; - } - return normalized.replace(/^[a-z]:\//i, '').replace(/^b\//, ''); + return file + .replace(/\\/g, '/') + .replace(/^[a-z]:\//i, '') + .replace(/^b\//, ''); } export async function readFileAtGitRef(repo, ref, relativePath) { try { diff --git a/dist/index.js b/dist/index.js index aec6136..e312d64 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import { fileURLToPath } from 'node:url'; import { runCapabilityDiff } from './diff.js'; -import { renderReport } from './report.js'; +import { renderReport, severityRank } from './report.js'; export async function main(argv = process.argv.slice(2)) { if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) { process.stdout.write(`${usage()}\n`); @@ -23,6 +23,10 @@ async function runDiffCommand(argv) { ? await runCapabilityDiff({ mode: 'directories', oldRoot: parsed.oldRoot, newRoot: parsed.newRoot }) : await runCapabilityDiff({ mode: 'git', repo: parsed.repo, base: parsed.base, head: parsed.head }); process.stdout.write(renderReport(report, parsed.format)); + if (severityRank[parsed.failOn] > 0 && severityRank[report.rating] >= severityRank[parsed.failOn]) { + process.stderr.write(`CapabilityEcho capability drift rating ${report.rating} meets fail-on threshold ${parsed.failOn}.\n`); + return 1; + } return 0; } function parseDiffArgs(argv) { @@ -32,6 +36,7 @@ function parseDiffArgs(argv) { let head; let repo = process.cwd(); let format = 'text'; + let failOn = 'none'; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; const value = argv[index + 1]; @@ -62,6 +67,14 @@ function parseDiffArgs(argv) { format = value; index += 1; } + else if (arg === '--fail-on') { + const normalized = (value ?? '').toLowerCase(); + if (!isRating(normalized)) { + return { ok: false, error: `Invalid --fail-on value: ${value ?? ''}. Use none, low, medium, high, or critical.` }; + } + failOn = normalized; + index += 1; + } else { return { ok: false, error: `Unknown argument: ${arg}` }; } @@ -78,7 +91,7 @@ function parseDiffArgs(argv) { if (!head) { return { ok: false, error: 'Missing required --head argument.' }; } - return { ok: true, mode: 'git', repo, base, head, format }; + return { ok: true, mode: 'git', repo, base, head, format, failOn }; } if (!oldRoot) { return { ok: false, error: 'Missing required --old argument or --base argument.' }; @@ -86,11 +99,14 @@ function parseDiffArgs(argv) { if (!newRoot) { return { ok: false, error: 'Missing required --new argument.' }; } - return { ok: true, mode: 'directories', oldRoot, newRoot, format }; + return { ok: true, mode: 'directories', oldRoot, newRoot, format, failOn }; } function isReportFormat(value) { return value === 'text' || value === 'markdown' || value === 'json' || value === 'github'; } +function isRating(value) { + return value === 'none' || value === 'low' || value === 'medium' || value === 'high' || value === 'critical'; +} const invokedPath = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false; if (invokedPath) { process.exitCode = await main(); @@ -98,7 +114,7 @@ if (invokedPath) { function usage() { return [ 'Usage:', - ' capabilityecho diff --old --new [--format text|markdown|json|github]', - ' capabilityecho diff --repo --base --head [--format text|markdown|json|github]' + ' capabilityecho diff --old --new [--format text|markdown|json|github] [--fail-on none|low|medium|high|critical]', + ' capabilityecho diff --repo --base --head [--format text|markdown|json|github] [--fail-on none|low|medium|high|critical]' ].join('\n'); } diff --git a/dist/report.js b/dist/report.js index 34ebd5c..2847447 100644 --- a/dist/report.js +++ b/dist/report.js @@ -6,7 +6,7 @@ const SURFACE_LABELS = { workflow: 'GitHub workflows', container: 'container builds' }; -const severityRank = { +export const severityRank = { none: 0, low: 1, medium: 2, diff --git a/src/action.ts b/src/action.ts index c70fe5d..0b12523 100644 --- a/src/action.ts +++ b/src/action.ts @@ -1,15 +1,7 @@ import { appendFile, readFile } from 'node:fs/promises'; import { runCapabilityDiff } from './diff.js'; import { GitDiffSetupError } from './git-diff.js'; -import { renderReport, type EchoRating } from './report.js'; - -const severityRank: Record = { - none: 0, - low: 1, - medium: 2, - high: 3, - critical: 4 -}; +import { renderReport, severityRank, type EchoRating } from './report.js'; export async function mainAction(env: NodeJS.ProcessEnv = process.env): Promise { const repo = getInput(env, 'repo') || env.GITHUB_WORKSPACE || process.cwd(); diff --git a/src/detectors/workflow-permissions.ts b/src/detectors/workflow-permissions.ts index bd94efb..ff8d226 100644 --- a/src/detectors/workflow-permissions.ts +++ b/src/detectors/workflow-permissions.ts @@ -227,6 +227,10 @@ function detectExternalCurl(added: AddedLine): Finding[] { return []; } + if (!referencesExternalUrlOrVariable(added.content)) { + return []; + } + return [ { kind: 'capability_echo.workflow_external_curl', @@ -241,6 +245,25 @@ function detectExternalCurl(added: AddedLine): Finding[] { ]; } +function referencesExternalUrlOrVariable(content: string): boolean { + const urls = content.match(/https?:\/\/[^\s'"`)]+/gi) ?? []; + for (const url of urls) { + if (!isLocalUrl(url)) { + return true; + } + } + + if (urls.length === 0 && /\$\{?\w|\$\{\{/.test(content)) { + return true; + } + + return false; +} + +function isLocalUrl(url: string): boolean { + return /^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:[/?#]|$)/i.test(url); +} + function detectSecretsInherit(added: AddedLine): Finding[] { if (!/^\s*secrets\s*:\s*inherit\s*(?:#.*)?$/i.test(added.content)) { return []; diff --git a/src/git-diff.ts b/src/git-diff.ts index 7ddd0fe..9f5fdab 100644 --- a/src/git-diff.ts +++ b/src/git-diff.ts @@ -240,20 +240,10 @@ function isExecError(error: unknown): error is Error & { code?: number | string; } function normalizeGitDiffPath(file: string): string { - const normalized = file.replace(/\\/g, '/'); - const markers = ['/src/', '/.github/workflows/', '/package.json']; - for (const marker of markers) { - const index = normalized.lastIndexOf(marker); - if (index >= 0) { - return normalized.slice(index + 1); - } - } - - if (normalized.endsWith('package.json')) { - return 'package.json'; - } - - return normalized.replace(/^[a-z]:\//i, '').replace(/^b\//, ''); + return file + .replace(/\\/g, '/') + .replace(/^[a-z]:\//i, '') + .replace(/^b\//, ''); } export async function readFileAtGitRef(repo: string, ref: string, relativePath: string): Promise { diff --git a/src/index.ts b/src/index.ts index 1e1318e..62486cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'; import { runCapabilityDiff } from './diff.js'; -import { renderReport, type ReportFormat } from './report.js'; +import { renderReport, severityRank, type EchoRating, type ReportFormat } from './report.js'; export async function main(argv = process.argv.slice(2)): Promise { if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) { @@ -31,12 +31,20 @@ async function runDiffCommand(argv: string[]): Promise { : await runCapabilityDiff({ mode: 'git', repo: parsed.repo, base: parsed.base, head: parsed.head }); process.stdout.write(renderReport(report, parsed.format)); + + if (severityRank[parsed.failOn] > 0 && severityRank[report.rating] >= severityRank[parsed.failOn]) { + process.stderr.write( + `CapabilityEcho capability drift rating ${report.rating} meets fail-on threshold ${parsed.failOn}.\n` + ); + return 1; + } + return 0; } type ParsedDiffArgs = - | { ok: true; mode: 'directories'; oldRoot: string; newRoot: string; format: ReportFormat } - | { ok: true; mode: 'git'; repo: string; base: string; head: string; format: ReportFormat } + | { ok: true; mode: 'directories'; oldRoot: string; newRoot: string; format: ReportFormat; failOn: EchoRating } + | { ok: true; mode: 'git'; repo: string; base: string; head: string; format: ReportFormat; failOn: EchoRating } | { ok: false; error: string }; function parseDiffArgs(argv: string[]): ParsedDiffArgs { @@ -46,6 +54,7 @@ function parseDiffArgs(argv: string[]): ParsedDiffArgs { let head: string | undefined; let repo = process.cwd(); let format: ReportFormat = 'text'; + let failOn: EchoRating = 'none'; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; @@ -72,6 +81,13 @@ function parseDiffArgs(argv: string[]): ParsedDiffArgs { } format = value; index += 1; + } else if (arg === '--fail-on') { + const normalized = (value ?? '').toLowerCase(); + if (!isRating(normalized)) { + return { ok: false, error: `Invalid --fail-on value: ${value ?? ''}. Use none, low, medium, high, or critical.` }; + } + failOn = normalized; + index += 1; } else { return { ok: false, error: `Unknown argument: ${arg}` }; } @@ -93,7 +109,7 @@ function parseDiffArgs(argv: string[]): ParsedDiffArgs { return { ok: false, error: 'Missing required --head argument.' }; } - return { ok: true, mode: 'git', repo, base, head, format }; + return { ok: true, mode: 'git', repo, base, head, format, failOn }; } if (!oldRoot) { @@ -104,13 +120,17 @@ function parseDiffArgs(argv: string[]): ParsedDiffArgs { return { ok: false, error: 'Missing required --new argument.' }; } - return { ok: true, mode: 'directories', oldRoot, newRoot, format }; + return { ok: true, mode: 'directories', oldRoot, newRoot, format, failOn }; } function isReportFormat(value: string | undefined): value is ReportFormat { return value === 'text' || value === 'markdown' || value === 'json' || value === 'github'; } +function isRating(value: string): value is EchoRating { + return value === 'none' || value === 'low' || value === 'medium' || value === 'high' || value === 'critical'; +} + const invokedPath = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false; if (invokedPath) { @@ -120,7 +140,7 @@ if (invokedPath) { function usage(): string { return [ 'Usage:', - ' capabilityecho diff --old --new [--format text|markdown|json|github]', - ' capabilityecho diff --repo --base --head [--format text|markdown|json|github]' + ' capabilityecho diff --old --new [--format text|markdown|json|github] [--fail-on none|low|medium|high|critical]', + ' capabilityecho diff --repo --base --head [--format text|markdown|json|github] [--fail-on none|low|medium|high|critical]' ].join('\n'); } diff --git a/src/report.ts b/src/report.ts index 2df8742..a4e9f1e 100644 --- a/src/report.ts +++ b/src/report.ts @@ -26,7 +26,7 @@ const SURFACE_LABELS: Record = { container: 'container builds' }; -const severityRank: Record = { +export const severityRank: Record = { none: 0, low: 1, medium: 2, diff --git a/test/cli-output.test.mjs b/test/cli-output.test.mjs index b0957d0..6e5bb53 100644 --- a/test/cli-output.test.mjs +++ b/test/cli-output.test.mjs @@ -92,3 +92,85 @@ test('clean fixture returns rating none', async () => { assert.equal(report.rating, 'none'); assert.equal(report.findingCount, 0); }); + +test('CLI exits 0 by default when findings exceed nothing', async () => { + const oldDir = join(testDir, 'fixtures', 'capability-drift', 'old'); + const newDir = join(testDir, 'fixtures', 'capability-drift', 'new'); + + const result = await execFileAsync( + process.execPath, + ['dist/index.js', 'diff', '--old', oldDir, '--new', newDir, '--format', 'json'], + { cwd: packageRoot } + ); + + assert.equal(typeof result.stdout, 'string'); + assert.ok(result.stdout.length > 0); +}); + +test('CLI --fail-on returns non-zero when rating meets threshold', async () => { + const oldDir = join(testDir, 'fixtures', 'capability-drift', 'old'); + const newDir = join(testDir, 'fixtures', 'capability-drift', 'new'); + + const result = await execFileAsync( + process.execPath, + [ + 'dist/index.js', + 'diff', + '--old', + oldDir, + '--new', + newDir, + '--format', + 'json', + '--fail-on', + 'HIGH' + ], + { cwd: packageRoot } + ).then( + ({ stdout, stderr }) => ({ code: 0, stdout, stderr }), + (error) => ({ + code: typeof error === 'object' && error && 'code' in error ? error.code : undefined, + stdout: typeof error === 'object' && error && 'stdout' in error ? String(error.stdout) : '', + stderr: typeof error === 'object' && error && 'stderr' in error ? String(error.stderr) : '' + }) + ); + + assert.equal(result.code, 1); + assert.match(result.stderr, /rating critical meets fail-on threshold high/); + const report = JSON.parse(result.stdout); + assert.equal(report.rating, 'critical'); +}); + +test('CLI --fail-on returns 0 when rating below threshold', async () => { + const oldDir = join(testDir, 'fixtures', 'clean', 'old'); + const newDir = join(testDir, 'fixtures', 'clean', 'new'); + + const result = await execFileAsync( + process.execPath, + ['dist/index.js', 'diff', '--old', oldDir, '--new', newDir, '--format', 'json', '--fail-on', 'high'], + { cwd: packageRoot } + ); + + const report = JSON.parse(result.stdout); + assert.equal(report.rating, 'none'); +}); + +test('CLI rejects unknown --fail-on value', async () => { + const oldDir = join(testDir, 'fixtures', 'clean', 'old'); + const newDir = join(testDir, 'fixtures', 'clean', 'new'); + + const result = await execFileAsync( + process.execPath, + ['dist/index.js', 'diff', '--old', oldDir, '--new', newDir, '--fail-on', 'bogus'], + { cwd: packageRoot } + ).then( + ({ stdout, stderr }) => ({ code: 0, stdout, stderr }), + (error) => ({ + code: typeof error === 'object' && error && 'code' in error ? error.code : undefined, + stderr: typeof error === 'object' && error && 'stderr' in error ? String(error.stderr) : '' + }) + ); + + assert.equal(result.code, 2); + assert.match(result.stderr, /Invalid --fail-on value/); +}); diff --git a/test/detectors.test.mjs b/test/detectors.test.mjs index 003a364..510963f 100644 --- a/test/detectors.test.mjs +++ b/test/detectors.test.mjs @@ -114,6 +114,65 @@ test('workflow detector flags secret-backed env vars used in external requests', assert.equal(exfilFinding.severity, 'high'); }); +test('workflow detector flags external curl with a literal URL', () => { + const findings = detectWorkflowPermissions([ + { + file: '.github/workflows/ci.yml', + line: 12, + content: ' - run: curl https://example.com/bootstrap.sh' + } + ]); + + assert.ok(findings.some((finding) => finding.kind === 'capability_echo.workflow_external_curl')); +}); + +test('workflow detector flags external curl when only a variable URL is present', () => { + const findings = detectWorkflowPermissions([ + { + file: '.github/workflows/ci.yml', + line: 12, + content: ' - run: curl -L $RELEASE_URL | tar xz' + } + ]); + + assert.ok(findings.some((finding) => finding.kind === 'capability_echo.workflow_external_curl')); +}); + +test('workflow detector skips localhost curl invocations', () => { + const findings = detectWorkflowPermissions([ + { + file: '.github/workflows/ci.yml', + line: 12, + content: ' - run: curl http://localhost:8080/health' + }, + { + file: '.github/workflows/ci.yml', + line: 13, + content: ' - run: wget http://127.0.0.1:9000/ready' + } + ]); + + assert.equal( + findings.filter((finding) => finding.kind === 'capability_echo.workflow_external_curl').length, + 0 + ); +}); + +test('workflow detector skips curl lines without URL or variable references', () => { + const findings = detectWorkflowPermissions([ + { + file: '.github/workflows/ci.yml', + line: 12, + content: ' - name: curl is required for the next step' + } + ]); + + assert.equal( + findings.filter((finding) => finding.kind === 'capability_echo.workflow_external_curl').length, + 0 + ); +}); + test('workflow detector flags inherited reusable workflow secrets', () => { const findings = detectWorkflowPermissions([ { diff --git a/test/git-diff.test.mjs b/test/git-diff.test.mjs index 31df5be..0765797 100644 --- a/test/git-diff.test.mjs +++ b/test/git-diff.test.mjs @@ -432,6 +432,45 @@ test('CLI flags Python external requests using existing source env secret variab } }); +test('CLI preserves monorepo source paths in findings and annotations', async () => { + const fx = await makeGitRepo({ + prefix: 'capabilityecho-monorepo-', + initialFiles: { + 'package.json': `${JSON.stringify({ name: 'monorepo-fixture', private: true }, null, 2)}\n`, + 'apps/web/src/api.ts': "export function hello() {\n return 'ok';\n}\n", + 'packages/core/src/util.ts': "export function tag() {\n return 'base';\n}\n", + }, + initialMessage: 'base monorepo', + }); + try { + const base = await fx.head(); + const head = await fx.commit( + { + 'package.json': `${JSON.stringify({ name: 'monorepo-fixture', private: true }, null, 2)}\n`, + 'apps/web/src/api.ts': + "export async function sync() {\n await fetch('https://api.example.com/v1/events');\n}\n", + 'packages/core/src/util.ts': + "export function tag() {\n return 'base';\n}\nexport async function pull() {\n await fetch('https://collector.example.com/pull');\n}\n", + }, + 'add external fetches in monorepo paths' + ); + + const report = await runDiff(fx.repo, base, head); + const webFinding = report.findings.find( + (finding) => finding.kind === 'capability_echo.external_fetch_added' && finding.file.includes('apps/web') + ); + const coreFinding = report.findings.find( + (finding) => finding.kind === 'capability_echo.external_fetch_added' && finding.file.includes('packages/core') + ); + assert.ok(webFinding, 'expected a finding under apps/web'); + assert.equal(webFinding.file, 'apps/web/src/api.ts'); + assert.ok(coreFinding, 'expected a finding under packages/core'); + assert.equal(coreFinding.file, 'packages/core/src/util.ts'); + } finally { + await fx.cleanup(); + } +}); + test('git diff exposes missing refs as setup errors', async () => { const fx = await makeGitRepo({ prefix: 'capabilityecho-git-setup-error-',