diff --git a/action.yml b/action.yml index 582e5f5..8563e1c 100644 --- a/action.yml +++ b/action.yml @@ -50,4 +50,4 @@ outputs: runs: using: node24 - main: dist/action.js + main: dist/action-bundle/index.js diff --git a/dist/action-bundle/index.js b/dist/action-bundle/index.js new file mode 100644 index 0000000..aa85af7 --- /dev/null +++ b/dist/action-bundle/index.js @@ -0,0 +1 @@ +import{createRequire as e}from"module";var t={929:(e,t,s)=>{s.a(e,(async(e,n)=>{try{s.d(t,{g:()=>mainAction});var i=s(455);var r=s.n(i);var o=s(938);var a=s(924);var c=s(665);const l={none:0,low:1,medium:2,high:3,critical:4};async function mainAction(e=process.env){const t=getInput(e,"repo")||e.GITHUB_WORKSPACE||process.cwd();const s=await readEvent(e);const n=getInput(e,"base")||getDefaultBase(e,s);const i=getInput(e,"head")||getDefaultHead(e,s);const r=getInput(e,"fail-on")||"none";const u=r.toLowerCase();if(!n||!i){writeError("CapabilityEcho needs base and head refs. Pass base/head inputs or run on pull_request with actions/checkout fetch-depth: 0.");return 2}if(!isRating(u)){writeError(`Invalid fail-on value '${r}'. Use none, low, medium, high, or critical.`);return 2}let f;try{f=await(0,o.X)({mode:"git",repo:t,base:n,head:i})}catch(e){if(e instanceof a.rd){writeError(`CapabilityEcho could not compare base '${e.base}' and head '${e.head}'. Ensure actions/checkout uses fetch-depth: 0, or pass refs that exist in the checkout through the \`base\` and \`head\` inputs.`);return 2}throw e}const p=(0,c.B)(f,"markdown");const d=(0,c.B)(f,"json");const h=JSON.stringify({rating:f.rating,hasFindings:f.findingCount>0,findingCount:f.findingCount,changedFileCount:f.changedFileCount,surfaceSummary:f.surfaceSummary,severitySummary:f.severitySummary,capabilitySummary:f.capabilitySummary,topRecommendations:f.topRecommendations});process.stdout.write(p);process.stdout.write((0,c.B)(f,"github"));await appendIfSet(e.GITHUB_STEP_SUMMARY,p);await writeOutput(e,"rating",f.rating);await writeOutput(e,"has-findings",String(f.findingCount>0));await writeOutput(e,"finding-count",String(f.findingCount));await writeOutput(e,"changed-file-count",String(f.changedFileCount));await writeOutput(e,"surface-summary",JSON.stringify(f.surfaceSummary));await writeOutput(e,"severity-summary",JSON.stringify(f.severitySummary));await writeOutput(e,"capability-summary",JSON.stringify(f.capabilitySummary));await writeOutput(e,"top-recommendations",JSON.stringify(f.topRecommendations));await writeOutput(e,"adoption-evidence",h);await writeOutput(e,"report-markdown",p);await writeOutput(e,"report-json",d);if(l[u]>0&&l[f.rating]>=l[u]){writeError(`CapabilityEcho capability drift rating ${f.rating} meets fail-on threshold ${u}.`);return 1}return 0}function getInput(e,t){const s=e[`INPUT_${t.replace(/ /g,"_").toUpperCase()}`];const n=e[`INPUT_${t.replace(/[- ]/g,"_").toUpperCase()}`];return(s||n||"").trim()}async function readEvent(e){if(!e.GITHUB_EVENT_PATH){return{}}try{const t=await(0,i.readFile)(e.GITHUB_EVENT_PATH,"utf8");const s=JSON.parse(t);return isRecord(s)?s:{}}catch{return{}}}function getDefaultBase(e,t){const s=t.pull_request;if(isRecord(s)&&isRecord(s.base)&&typeof s.base.sha==="string"){return s.base.sha}if(typeof t.before==="string"){return t.before}return e.DEFAULT_BASE||""}function getDefaultHead(e,t){const s=t.pull_request;if(isRecord(s)&&isRecord(s.head)&&typeof s.head.sha==="string"){return s.head.sha}if(typeof t.after==="string"){return t.after}return e.DEFAULT_HEAD||e.GITHUB_SHA||""}async function writeOutput(e,t,s){if(!e.GITHUB_OUTPUT){return}if(s.includes("\n")||s.includes("\r")){const n=outputDelimiter(t,s);const r=s.endsWith("\n")?s:`${s}\n`;await(0,i.appendFile)(e.GITHUB_OUTPUT,`${t}<<${n}\n${r}${n}\n`,"utf8");return}await(0,i.appendFile)(e.GITHUB_OUTPUT,`${t}=${s}\n`,"utf8")}function outputDelimiter(e,t){const s=e.replace(/[^A-Za-z0-9_]+/g,"_");let n=`capabilityecho_${s}_EOF`;let i=1;while(t.includes(n)){n=`capabilityecho_${s}_EOF_${i}`;i+=1}return n}async function appendIfSet(e,t){if(!e){return}await(0,i.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()}n()}catch(u){n(u)}}),1)},938:(t,s,n)=>{n.d(s,{X:()=>runCapabilityDiff});var i=n(431);function detectDockerfileCapability(e){const t=[];for(const s of e){if(!(0,i.pZ)(s.file)||(0,i.w5)(s.content)){continue}t.push(...detectRemoteAdd(s));t.push(...detectPipeToShell(s))}return t}function detectRemoteAdd(e){if(!/^\s*ADD\s+https?:\/\//i.test(e.content)){return[]}return[{kind:"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:"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 s=[];const n=collectSecretVariables(e,t);for(const t of e){if(!(0,i.Mn)(t.file)||(0,i.w5)(t.content)){continue}const e=(0,i.hl)(t.file);s.push(...detectFetch(t,e));s.push(...detectSecretExfil(t,e,n.get(t.file)??new Set));s.push(...detectSubprocess(t,e));s.push(...detectDynamicEval(t,e))}return s}function collectSecretVariables(e,t){const s=new Map;for(const t of e){if(!(0,i.Mn)(t.file)){continue}addSecretVariable(s,t.file,t.content)}for(const[e,n]of Object.entries(t)){if(!(0,i.Mn)(e)){continue}for(const t of n.split(/\r?\n/)){addSecretVariable(s,e,t)}}return s}function addSecretVariable(e,t,s){const n=s.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(!n){return}const i=e.get(t)??new Set;i.add(n[1]);e.set(t,i)}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:"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,s){if(!isExternalHttpRequest(e.content)||!referencesEnvSecret(e.content)&&!referencesSecretVariable(e.content,s)){return[]}return[{kind:"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:"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:"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=n(455);var o=n(760);const a=e(import.meta.url)("node:fs");function stripJsonComments(e){const t=e.length;const s=new Array(t);let n=0;let i=null;let r=false;while(n=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=this.descendTablePath(e,true);const s=e.join("\0");if(this.definedTables.has(s)){throw new Error(`Duplicate table definition: [${e.join(".")}]`)}this.definedTables.add(s);this.current=t}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 s=e[e.length-1];let n=t[s];if(n===undefined){n=[];t[s]=n;this.aotPaths.add(e.join("\0"))}else if(!Array.isArray(n)){throw new Error(`Key ${e.join(".")} is not an array-of-tables`)}const i={};n.push(i);this.current=i}descendTablePath(e,t){let s=this.root;for(let t=0;t0&&a.length===0){const n=new RegExp(`^\\s*${escapeForRegex(t)}\\s*=`);if(n.test(s))return e+1}}return 0}function findLineByRegex(e,t,s){const n=s?e.slice(s.start,s.end):e;const i=t.exec(n);if(!i)return 0;const r=(s?s.start:0)+i.index;return lineOfOffset(e,r)}function lineOfOffset(e,t){let s=1;for(let n=0;n=n)break;const i=e[s];if(i==='"'){s++;const i=s;while(s`${e}=${t}`)).sort();t.push(`env=${s.join("|")}`)}return t.join("\n")}function normalizeExecutable(e){const t=e.trim();const s=t.replace(/\\/g,"/");const n=s.replace(/\.(cmd|exe|bat|ps1)$/i,"");return n}function normalizePath(e){return e.trim().replace(/\\/g,"/").replace(/\/+$/,"")}const u=new Set(["-y","--yes"]);function canonicalizeArgs(e){const t=e.filter((e=>!u.has(e)));const s=[];const n=[];let i=false;for(let e=0;eet?1:0));const r=[...s];for(const[e,t]of n){if(e.startsWith("__pos__"))r.push(e.slice("__pos__".length));else if(t===null)r.push(e);else r.push(`${e}=${t}`)}return r}const f={low:1,medium:2,high:3,critical:4};function rankSeverity(e){return f[e]}function passesSeverityThreshold(e,t){return rankSeverity(e)>=rankSeverity(t)}function anyAtOrAbove(e,t){for(const s of e){if(passesSeverityThreshold(s.severity,t))return true}return false}function emitFindingAnnotation(e){const t=e.severity==="critical"||e.severity==="high"?"error":"warning";const s=[];if(e.location?.file)s.push(`file=${escapeProperty(e.location.file)}`);if(e.location?.line!=null)s.push(`line=${e.location.line}`);if(e.location?.column!=null)s.push(`col=${e.location.column}`);if(e.location?.endLine!=null)s.push(`endLine=${e.location.endLine}`);if(e.location?.endColumn!=null)s.push(`endColumn=${e.location.endColumn}`);s.push(`title=${escapeProperty(`[${e.kind}] ${e.severity}`)}`);const n=escapeData(e.message);return`::${t} ${s.join(",")}::${n}`}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")}async function readJsonObject(e){return(await discovery_readJsonObjectWithSource(e)).json}async function discovery_readJsonObjectWithSource(e){try{const t=await readFile(e,"utf8");const s=JSON.parse(t);return{json:isRecord(s)?s:{},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 s=lineOfJsonKey(e,t);return s===0?undefined:s}function discovery_lineOfJsonStringValue(e,t){const s=lineOfJsonStringValue(e,t);return s===0?undefined:s}function isNodeError(e){return e instanceof Error&&"code"in e}var p=n(924);const d=["postinstall","preinstall","prepare","install"];async function detectPackageScripts(e){const t=e.mode==="directories"?await(0,p.kf)(e.newRoot):await listChangedPackageJsonFiles(e.repo,e.base,e.head);const s=[];for(const n of t){const t=await readScriptsAt(e,n,"old");const i=await readScriptsAt(e,n,"new");const r=await readPackageTextAt(e,n,"new");s.push(...compareScripts(n,t,i,r))}return s}async function listChangedPackageJsonFiles(e,t,s){return(await(0,p.Eh)(e,t,s)).filter(i.xr)}async function readScriptsAt(e,t,s){const n=await readPackageTextAt(e,t,s);if(!n){return{}}try{const e=JSON.parse(n);if(!isRecord(e)||!isRecord(e.scripts)){return{}}const t={};for(const[s,n]of Object.entries(e.scripts)){if(typeof n==="string"){t[s]=n}}return t}catch{return{}}}async function readPackageTextAt(e,t,s){if(e.mode==="directories"){const n=s==="old"?e.oldRoot:e.newRoot;try{return await(0,r.readFile)(configPath(n,t),"utf8")}catch{return""}}const n=s==="old"?e.base:e.head;return await(0,p.p9)(e.repo,n,t)??""}function compareScripts(e,t,s,n){const i=[];for(const r of d){const o=s[r];if(!o){continue}const a=t[r];if(a===o){continue}const c=discovery_lineOfJsonKey(n,r)??discovery_lineOfJsonStringValue(n,o);i.push({kind:"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."});i.push(...analyzeScriptContent(e,r,o,n))}for(const[r,o]of Object.entries(s)){if(d.includes(r)){continue}const s=t[r];if(s===o){continue}i.push(...analyzeScriptContent(e,r,o,n))}return i}function analyzeScriptContent(e,t,s,n){const i=[];const r=discovery_lineOfJsonStringValue(n,s)??discovery_lineOfJsonKey(n,t);if(/(?:curl[^\n|]*\|\s*(?:ba)?sh|wget[^\n|]*\|\s*sh|Invoke-Expression|iex\s*\()/i.test(s)){i.push({kind:"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(s)||/\bnpx\b(?![^\s]*@\d+\.\d+\.\d+)/i.test(s)){i.push({kind:"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 i}const h=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 m=new Set(["@segment/analytics-node","mixpanel","amplitude-js","posthog-js","@sentry/node","@sentry/browser"]);const w=["dependencies","devDependencies","optionalDependencies","peerDependencies"];async function detectPackageDeps(e){const t=e.mode==="directories"?await(0,p.kf)(e.newRoot):await listChangedPackageJsonFiles(e.repo,e.base,e.head);const s=[];for(const n of t){const t=await readPackageTextAt(e,n,"old");const i=await readPackageTextAt(e,n,"new");s.push(...compareDeps(n,t,i))}return s}function compareDeps(e,t,s){const n=readAllDeps(t);const i=readAllDeps(s);const r=[];for(const[t,o]of i.entries()){if(n.has(t)){continue}if(h.has(t)){r.push({kind:"high_capability_dep_added",surface:"package",severity:"high",file:e,line:discovery_lineOfJsonStringValue(s,o)??discovery_lineOfJsonKey(s,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(m.has(t)){r.push({kind:"telemetry_dep_added",surface:"package",severity:"medium",file:e,line:discovery_lineOfJsonStringValue(s,o)??discovery_lineOfJsonKey(s,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 s;try{s=JSON.parse(e)}catch{return t}if(!isRecord(s)){return t}for(const e of w){const n=s[e];if(!isRecord(n)){continue}for(const[e,s]of Object.entries(n)){if(typeof s==="string"){t.set(e,s)}}}return t}function detectPyCapability(e,t={}){const s=[];const n=py_capability_collectSecretVariables(e,t);for(const t of e){if(!(0,i.UD)(t.file)||(0,i.w5)(t.content)){continue}const e=(0,i.hl)(t.file);s.push(...detectPyNetwork(t,e));s.push(...detectPySecretExfil(t,e,n.get(t.file)??new Set));s.push(...detectPySubprocess(t,e));s.push(...detectPyDynamicExec(t,e));s.push(...detectPyUnsafeDeserialize(t,e))}return s}function py_capability_collectSecretVariables(e,t){const s=new Map;for(const t of e){if(!(0,i.UD)(t.file)){continue}py_capability_addSecretVariable(s,t.file,t.content)}for(const[e,n]of Object.entries(t)){if(!(0,i.UD)(e)){continue}for(const t of n.split(/\r?\n/)){py_capability_addSecretVariable(s,e,t)}}return s}function py_capability_addSecretVariable(e,t,s){const n=s.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(!n){return}const i=e.get(t)??new Set;i.add(n[1]);e.set(t,i)}function detectPyNetwork(e,t){const s=/\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(!s.test(e.content)){return[]}if(!/(?:https?:\/\/|['"]https?:\/\/)/i.test(e.content)){return[]}return[{kind:"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,s){if(!isPyExternalRequest(e.content)||!referencesPyEnvSecret(e.content)&&!py_capability_referencesSecretVariable(e.content,s)){return[]}return[{kind:"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 s=/\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(!s.test(e.content)){return[]}return[{kind:"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 s=/\beval\s*\(|\bexec\s*\(|\bcompile\s*\(|\b__import__\s*\(|\bimportlib\.import_module\s*\(/i;if(!s.test(e.content)){return[]}return[{kind:"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 s=/\bpickle\.(?:load|loads)\s*\(|\bmarshal\.(?:load|loads)\s*\(|\byaml\.load\s*\((?![^)]*Loader\s*=\s*(?:yaml\.)?SafeLoader)/i;if(!s.test(e.content)){return[]}return[{kind:"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 s of e){if(!(0,i.Z7)(s.file)||(0,i.w5)(s.content)){continue}t.push(...shell_capability_detectPipeToShell(s));t.push(...detectExternalDownload(s))}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:"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:"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 g=/^\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 s=[];const n=new Set(e.filter((e=>(0,i.Kr)(e.file)&&isPullRequestTargetLine(e.content))).map((e=>e.file)));const r=collectSecretEnvVars(e,t);for(const[e,s]of Object.entries(t)){if((0,i.Kr)(e)&&hasPullRequestTargetWorkflow(s)){n.add(e)}}for(const t of e){if(!(0,i.Kr)(t.file)){continue}s.push(...detectPullRequestTarget(t));s.push(...detectPullRequestHeadCheckoutOnTarget(t,n.has(t.file)));s.push(...detectSelfHostedRunner(t));s.push(...detectMutableActionRef(t));s.push(...detectWritePermissions(t));s.push(...detectExternalCurl(t));s.push(...detectSecretsInherit(t));s.push(...workflow_permissions_detectSecretExfil(t,r.get(t.file)??new Set));s.push(...detectDockerHostControl(t))}return s}function collectSecretEnvVars(e,t){const s=new Map;for(const t of e){if(!(0,i.Kr)(t.file)){continue}addSecretEnvVar(s,t.file,t.content)}for(const[e,n]of Object.entries(t)){if(!(0,i.Kr)(e)){continue}for(const t of n.split(/\r?\n/)){addSecretEnvVar(s,e,t)}}return s}function addSecretEnvVar(e,t,s){const n=s.match(/^\s*([A-Z_][A-Z0-9_]*)\s*:\s*.*\$\{\{\s*secrets\./i);if(!n){return}const i=e.get(t)??new Set;i.add(n[1]);e.set(t,i)}function detectWritePermissions(e){const t=e.content;if(!/permissions\s*:/i.test(t)&&!g.test(t)){return[]}if(g.test(t)){return[{kind:"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:"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:"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:"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:"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 s=t.lastIndexOf("@");if(s===-1){return[]}const n=t.slice(s+1);if(!isMutableActionVersionRef(n)){return[]}return[{kind:"workflow_mutable_action_ref",surface:"workflow",severity:"medium",file:e.file,line:e.line,subject:"GitHub Actions mutable action reference",message:"Workflow uses a mutable remote action reference.",recommendation:"Pin third-party actions to a reviewed commit SHA before merge."}]}function extractWorkflowUsesRef(e){return e.match(/^\s*(?:-\s*)?uses\s*:\s*['"]?([^'"\s#]+)['"]?/i)?.[1]}function isLocalActionRef(e){return e.startsWith("./")||e.startsWith("../")||e.startsWith("/")}function isMutableActionVersionRef(e){return/^(main|master|trunk|develop|dev|latest|head)$/i.test(e)}function detectExternalCurl(e){if(!/\b(curl|wget|Invoke-WebRequest|fetch\s*\()/i.test(e.content)){return[]}return[{kind:"workflow_external_curl",surface:"workflow",severity:"medium",file:e.file,line:e.line,subject:"Workflow external request",message:"Workflow step performs an external network request.",recommendation:"Verify the URL, payload, and whether the request is necessary in CI."}]}function detectSecretsInherit(e){if(!/^\s*secrets\s*:\s*inherit\s*(?:#.*)?$/i.test(e.content)){return[]}return[{kind:"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 s=e.content;const n=/\$\{\{\s*secrets\.|\$\{?\s*secrets\.|env\.[A-Z0-9_]+/i.test(s)||[...t].some((e=>referencesShellVariable(s,e)));const i=/\b(curl|wget|Invoke-WebRequest|fetch\s*\()/i.test(s);const r=/\|\s*(bash|sh|powershell|pwsh)/i.test(s);if(!n||!i){return[]}return[{kind:"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 s=workflow_permissions_escapeRegExp(t);return new RegExp(String.raw`(?:\$\{${s}\}|\$${s}\b|%${s}%)`).test(e)}function workflow_permissions_escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function detectDockerHostControl(e){const t=[];const s=e.content;if(/\/var\/run\/docker\.sock(?::\/var\/run\/docker\.sock)?/i.test(s)){t.push({kind:"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(s)){t.push({kind:"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 y=n(665);async function runCapabilityDiff(e){const t=e.mode==="directories"?await(0,p.BH)(e.oldRoot,e.newRoot):await(0,p._u)(e.repo,e.base,e.head);const s=e.mode==="directories"?{mode:"directories",oldRoot:e.oldRoot,newRoot:e.newRoot}:{mode:"git",repo:e.repo,base:e.base,head:e.head};const[n,i]=await Promise.all([detectPackageScripts(s),detectPackageDeps(s)]);const r=[...detectWorkflowPermissions(t.addedLines,t.newFileContents),...detectDockerfileCapability(t.addedLines),...detectJsCapability(t.addedLines,t.newFileContents),...detectPyCapability(t.addedLines,t.newFileContents),...detectShellCapability(t.addedLines),...n,...i];return(0,y.L)(r,t)}},924:(t,s,n)=>{n.d(s,{rd:()=>GitDiffSetupError,BH:()=>collectDirectoryDiff,_u:()=>collectGitDiff,Eh:()=>listGitChangedFiles,kf:()=>listPackageJsonFiles,p9:()=>readFileAtGitRef});const i=e(import.meta.url)("node:child_process");var r=n(455);var o=n(760);const a=e(import.meta.url)("node:util");var c=n(431);const l=(0,a.promisify)(i.execFile);const u=["source","package","workflow","container"];class GitDiffSetupError extends Error{base;head;constructor(e,t,s){super(e);this.base=t;this.head=s;this.name="GitDiffSetupError"}}async function collectDirectoryDiff(e,t){const s=await listScannableFiles(t);const n=[];const i=new Set;const a={};for(const c of s){const s=(0,o.join)(e,c);const l=(0,o.join)(t,c);const u=await(0,r.readFile)(l,"utf8");const f=await runGitNoIndexDiff(s,l);if(f.trim()){i.add(c);a[c]=u;n.push(...parseUnifiedDiff(f,c));continue}let p="";try{p=await(0,r.readFile)(s,"utf8")}catch{p=""}if(p===u){continue}i.add(c);a[c]=u;if(!p){n.push(...allLinesAsAdded(c,u))}}return{addedLines:n,changedFileCount:i.size,scannedSurfaces:surfacesForFiles([...i]),newFileContents:a}}async function collectGitDiff(e,t,s){const n=await gitRefExists(e,t);const i=await gitRefExists(e,s);if(!n||!i){throw new GitDiffSetupError(`CapabilityEcho could not compare base '${t}' and head '${s}'.`,t,s)}const r=await listGitChangedFiles(e,t,s);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}..${s}`,"--",...o],{encoding:"utf8",maxBuffer:20*1024*1024});const u=await readChangedFilesAtRef(e,s,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 s=[];let n="";let i=0;for(const r of e.split(/\r?\n/)){if(r.startsWith("+++ ")){const e=r.slice(4).trim();if(e.startsWith("b/")){n=e.slice(2)}else if(e==="/dev/null"){n=""}else{n=e}continue}if(r.startsWith("@@")){const e=r.match(/\+(\d+)(?:,(\d+))?/);i=e?Number.parseInt(e[1],10):0;continue}if(!n||n==="/dev/null"){continue}if(r.startsWith("+")&&!r.startsWith("+++")){s.push({file:(t??normalizeGitDiffPath(n)).replace(/\\/g,"/"),line:i,content:r.slice(1)});i+=1;continue}if(r.startsWith("-")&&!r.startsWith("---")){continue}if(r.startsWith(" ")||r.startsWith("\\")){i+=1}}return s}async function listScannableFiles(e,t=""){const s=await(0,r.readdir)((0,o.join)(e,t),{withFileTypes:true});const n=[];for(const i of s){if(i.name==="node_modules"||i.name===".git"){continue}const s=t?`${t}/${i.name}`:i.name;if(i.isDirectory()){n.push(...await listScannableFiles(e,s));continue}if((0,c.Iy)(s)){n.push(s.replace(/\\/g,"/"))}}return n}async function listGitChangedFiles(e,t,s){const{stdout:n}=await l("git",["-C",e,"diff","--name-only",`${t}..${s}`],{encoding:"utf8",maxBuffer:10*1024*1024});return n.split(/\r?\n/).map((e=>e.trim())).filter(Boolean).map((e=>e.replace(/\\/g,"/")))}async function runGitNoIndexDiff(e,t){try{const{stdout:s}=await l("git",["diff","--no-index","-U0",e,t],{encoding:"utf8",maxBuffer:10*1024*1024});return s}catch(e){if(isExecError(e)&&typeof e.stdout==="string"){return e.stdout}return""}}function allLinesAsAdded(e,t){const s=t.split(/\r?\n/);return s.map(((t,s)=>({file:e.replace(/\\/g,"/"),line:s+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 s of e){const e=(0,c.qk)(s);if(e){t.add(e)}}return u.filter((e=>t.has(e)))}function isExecError(e){return e instanceof Error&&"code"in e}function normalizeGitDiffPath(e){const t=e.replace(/\\/g,"/");const s=["/src/","/.github/workflows/","/package.json"];for(const e of s){const s=t.lastIndexOf(e);if(s>=0){return t.slice(s+1)}}if(t.endsWith("package.json")){return"package.json"}return t.replace(/^[a-z]:\//i,"").replace(/^b\//,"")}async function readFileAtGitRef(e,t,s){try{const{stdout:n}=await l("git",["-C",e,"show",`${t}:${s}`],{encoding:"utf8",maxBuffer:10*1024*1024});return n}catch(e){if(isExecError(e)){return null}throw e}}async function readChangedFilesAtRef(e,t,s){const n=await Promise.all(s.map((async s=>{const n=await readFileAtGitRef(e,t,s);return n===null?undefined:[s,n]})));return Object.fromEntries(n.filter((e=>e!==undefined)))}async function listPackageJsonFiles(e,t=""){const s=await(0,r.readdir)((0,o.join)(e,t),{withFileTypes:true});const n=[];for(const i of s){if(i.name==="node_modules"||i.name===".git"){continue}const s=t?`${t}/${i.name}`:i.name;if(i.isDirectory()){n.push(...await listPackageJsonFiles(e,s));continue}if(i.name==="package.json"){n.push(s.replace(/\\/g,"/"))}}return n}function relativeFromRoots(e,t){return relative(e,t).replace(/\\/g,"/")}},431:(e,t,s)=>{s.d(t,{Iy:()=>isScannable,Kr:()=>isWorkflowFile,Mn:()=>isJsFile,UD:()=>isPyFile,Z7:()=>isShellFile,hl:()=>isTestFile,pZ:()=>isDockerfile,qk:()=>surfaceForPath,w5:()=>isCommentLine,xr:()=>isPackageJsonFile});const n=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(n.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 s=t.split("/").pop()??t;return/^Dockerfile(?:\..+)?$/i.test(s)}},665:(e,t,s)=>{s.d(t,{B:()=>renderReport,L:()=>createReport});const n=["AI-agent config"];const i=["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={external_fetch_added:"external network fetch calls",source_secret_exfil_pattern:"source secret exfiltration patterns",subprocess_spawn_added:"subprocess or shell spawn calls",dynamic_eval_added:"dynamic code execution",shell_pipe_to_shell:"shell pipe-to-shell downloads",shell_external_download:"shell external downloads",dockerfile_remote_add:"Dockerfile remote ADD instructions",dockerfile_pipe_to_shell:"Dockerfile pipe-to-shell builds",workflow_permission_write:"GitHub Actions write permissions",workflow_pull_request_target:"GitHub Actions pull_request_target triggers",workflow_pr_head_checkout_on_target:"GitHub Actions PR-head checkout under pull_request_target",workflow_self_hosted_runner:"GitHub Actions self-hosted runners",workflow_mutable_action_ref:"GitHub Actions mutable action references",workflow_secrets_inherit:"GitHub Actions inherited secrets",workflow_external_curl:"workflow external network requests",workflow_secret_exfil_pattern:"workflow secret exfiltration patterns",workflow_docker_socket_mount:"workflow Docker socket mounts",workflow_privileged_container:"workflow privileged containers",lifecycle_script_added:"npm lifecycle scripts",script_pipe_to_shell:"pipe-to-shell install scripts",script_network_command:"network or publish npm scripts",high_capability_dep_added:"high-capability dependency additions",telemetry_dep_added:"telemetry dependency additions",unsafe_deserialize_added:"unsafe deserialization"};function createReport(e,t){return{rating:rateFindings(e),findingCount:e.length,changedFileCount:t.changedFileCount,scannedSurfaces:t.scannedSurfaces,excludedSurfaces:[...n],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 s of e){t.add(a[s.kind]??s.kind)}return[...t]}function buildTopRecommendations(e){const t=new Set;const s=e.map(((e,t)=>({finding:e,index:t}))).sort(((e,t)=>{const s=o[t.finding.severity]-o[e.finding.severity];return s===0?e.index-t.index:s}));for(const{finding:e}of s){t.add(e.recommendation);if(t.size===3){break}}return[...t]}function rateFindings(e){let t="none";for(const s of e){if(o[s.severity]>o[t]){t=s.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 s of e.topRecommendations){t.push(`- ${s}`)}t.push("")}t.push("## Review summary","");t.push("| Surface | Findings |");t.push("| --- | ---: |");for(const s of["source","package","workflow","container"]){t.push(`| ${r[s]} | ${e.surfaceSummary[s]} |`)}t.push("");t.push("| Severity | Findings |");t.push("| --- | ---: |");for(const s of i){t.push(`| ${capitalize(s)} | ${e.severitySummary[s]} |`)}t.push("");if(e.capabilitySummary.length>0){t.push("## Capability summary","");for(const s of e.capabilitySummary){t.push(`- ${s}`)}t.push("")}for(const s of i){const n=e.findings.filter((e=>e.severity===s));if(n.length===0){continue}t.push(`## ${capitalize(s)}`,"");for(const e of n){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 s of e.findings){t.push(`[${s.severity.toUpperCase()}] ${s.subject} (${r[s.surface]}): ${s.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 s=`${e.message} Recommendation: ${e.recommendation}`;const n=[`file=${escapeProperty(e.file)}`];if(e.line&&e.line>0){n.push(`line=${e.line}`)}n.push(`title=${escapeProperty(t)}`);return`::warning ${n.join(",")}::${escapeMessage(s)}`})).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 s={};function __nccwpck_require__(e){var n=s[e];if(n!==undefined){return n.exports}var i=s[e]={exports:{}};var r=true;try{t[e](i,i.exports,__nccwpck_require__);r=false}finally{if(r)delete s[e]}return i.exports}(()=>{var e=typeof Symbol==="function"?Symbol("webpack queues"):"__webpack_queues__";var t=typeof Symbol==="function"?Symbol("webpack exports"):"__webpack_exports__";var s=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=n=>n.map((n=>{if(n!==null&&typeof n==="object"){if(n[e])return n;if(n.then){var i=[];i.d=0;n.then((e=>{r[t]=e;resolveQueue(i)}),(e=>{r[s]=e;resolveQueue(i)}));var r={};r[e]=e=>e(i);return r}}var o={};o[e]=e=>{};o[t]=n;return o}));__nccwpck_require__.a=(n,i,r)=>{var o;r&&((o=[]).d=-1);var a=new Set;var c=n.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=>{})));n.exports=p;i((n=>{l=wrapDeps(n);var i;var getResult=()=>l.map((e=>{if(e[s])throw e[s];return e[t]}));var r=new Promise((t=>{i=()=>t(getResult);i.r=0;var fnQueue=e=>e!==o&&!a.has(e)&&(a.add(e),e&&!e.d&&(i.r++,e.push(i)));l.map((t=>t[e](fnQueue)))}));return i.r?r:getResult()}),(e=>(e?f(p[s]=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 s in t){if(__nccwpck_require__.o(t,s)&&!__nccwpck_require__.o(e,s)){Object.defineProperty(e,s,{enumerable:true,get:t[s]})}}}})();(()=>{__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 n=__nccwpck_require__(929);n=await n;var i=n.g;export{i as mainAction}; \ No newline at end of file diff --git a/dist/action-bundle/package.json b/dist/action-bundle/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/dist/action-bundle/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/package-lock.json b/package-lock.json index f89a697..0544865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@types/node": "^24.0.0", + "@vercel/ncc": "^0.38.4", "typescript": "^5.9.3" } }, @@ -29,6 +30,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, "node_modules/agent-gov-core": { "version": "0.1.2", "resolved": "git+ssh://git@github.com/Conalh/agent-gov-core.git#db05618adbd84a503df9b475c49ad86ff161c62e", diff --git a/package.json b/package.json index e84632a..7694493 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "capabilityecho": "./dist/index.js" }, "scripts": { - "build": "tsc -p tsconfig.json", + "build": "tsc -p tsconfig.json && npm run bundle-action", + "bundle-action": "ncc build src/action.ts -o dist/action-bundle --minify", "test": "node --test test/*.test.mjs" }, "dependencies": { @@ -15,6 +16,7 @@ }, "devDependencies": { "@types/node": "^24.0.0", + "@vercel/ncc": "^0.38.4", "typescript": "^5.9.3" }, "license": "MIT" diff --git a/test/workflow.test.mjs b/test/workflow.test.mjs index 00054a3..4661f8c 100644 --- a/test/workflow.test.mjs +++ b/test/workflow.test.mjs @@ -29,7 +29,7 @@ test('action.yml exposes capability drift outputs', async () => { test('action.yml runs the checked-in JavaScript action without installing PR-local scripts first', async () => { const action = await readFile(join(packageRoot, 'action.yml'), 'utf8'); - assert.match(action, /runs:\s*\r?\n\s+using: node24\r?\n\s+main: dist\/action\.js/); + assert.match(action, /runs:\s*\r?\n\s+using: node24\r?\n\s+main: dist\/action-bundle\/index\.js/); assert.doesNotMatch(action, /using: composite/); assert.doesNotMatch(action, /npm ci/); assert.doesNotMatch(action, /npm run build/);