diff --git a/action.yml b/action.yml index b8b9b63..b61468c 100644 --- a/action.yml +++ b/action.yml @@ -97,6 +97,16 @@ inputs: hardening-disabled-rules: description: Comma-separated list of hardening rule IDs to disable required: false + fail-on-detected-risk: + description: | + When true (the default in prevent mode), fail the step if the cimon agent + reported a detected risk on shutdown — including cases where the rule + fired but the SIGKILL did not land in time (short-lived processes). This + ensures the workflow surface reflects what cimon prevented, instead of + relying solely on the offending process exiting non-zero. + Has no effect in detect mode (prevent=false). + required: false + default: 'true' runs: using: node24 diff --git a/dist/post/index.js b/dist/post/index.js index e29987b..a754d98 100644 --- a/dist/post/index.js +++ b/dist/post/index.js @@ -127871,6 +127871,46 @@ async function run(config) { if (retval !== 0) { throw new Error(`Failed stopping Cimon process: ${retval}`); } + + // If the agent reported a detected risk in prevent mode, surface it as + // a step failure. The offending process may have already exited + // non-zero (in which case the workflow is already red), but a + // sufficiently short-lived process can complete before the kill lands, + // leaving the workflow green even though the rule fired. This check + // closes that gap so the workflow always reflects what cimon prevented. + const failOnDetectedRisk = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('fail-on-detected-risk'); + const preventMode = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('prevent'); + if (failOnDetectedRisk && preventMode) { + const risk = parseDetectedRisks(stopOutput); + if (risk && risk !== 'NoRisk') { + throw new Error( + `Cimon detected a risk in prevent mode (${risk}). See the ` + + `cimon log output and the job summary for the specific rule ` + + `and evidence. Set fail-on-detected-risk: false to disable ` + + `this check.` + ); + } + } +} + +/** + * Extract the latest `detectedRisks` value from the cimon agent's + * structured-JSON stop output. Returns null if not present. + * + * The agent emits one log line near shutdown shaped like: + * {"level":"info",...,"detectedRisks":"PreventedRisk","message":"Terminating execution"} + * + * In rare cases multiple such lines appear (e.g. multiple executions); + * we take the last one so the most-recent state wins. + */ +function parseDetectedRisks(text) { + const re = /"detectedRisks"\s*:\s*"([^"]+)"/g; + let match; + let last = null; + while ((match = re.exec(text)) !== null) { + last = match[1]; + } + return last; } /** diff --git a/src/post/index.js b/src/post/index.js index 0fb0d38..6a5ffec 100644 --- a/src/post/index.js +++ b/src/post/index.js @@ -140,6 +140,46 @@ async function run(config) { if (retval !== 0) { throw new Error(`Failed stopping Cimon process: ${retval}`); } + + // If the agent reported a detected risk in prevent mode, surface it as + // a step failure. The offending process may have already exited + // non-zero (in which case the workflow is already red), but a + // sufficiently short-lived process can complete before the kill lands, + // leaving the workflow green even though the rule fired. This check + // closes that gap so the workflow always reflects what cimon prevented. + const failOnDetectedRisk = core.getBooleanInput('fail-on-detected-risk'); + const preventMode = core.getBooleanInput('prevent'); + if (failOnDetectedRisk && preventMode) { + const risk = parseDetectedRisks(stopOutput); + if (risk && risk !== 'NoRisk') { + throw new Error( + `Cimon detected a risk in prevent mode (${risk}). See the ` + + `cimon log output and the job summary for the specific rule ` + + `and evidence. Set fail-on-detected-risk: false to disable ` + + `this check.` + ); + } + } +} + +/** + * Extract the latest `detectedRisks` value from the cimon agent's + * structured-JSON stop output. Returns null if not present. + * + * The agent emits one log line near shutdown shaped like: + * {"level":"info",...,"detectedRisks":"PreventedRisk","message":"Terminating execution"} + * + * In rare cases multiple such lines appear (e.g. multiple executions); + * we take the last one so the most-recent state wins. + */ +function parseDetectedRisks(text) { + const re = /"detectedRisks"\s*:\s*"([^"]+)"/g; + let match; + let last = null; + while ((match = re.exec(text)) !== null) { + last = match[1]; + } + return last; } /**