mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-30 13:35:12 +02:00
fix: close release review gaps and expose failing CI suites
This commit is contained in:
@@ -255,6 +255,10 @@ function resolveManagedHookCommands(managedHooks, targetRoot) {
|
||||
const encodedRoot = Buffer.from(targetRoot, 'utf8').toString('base64');
|
||||
const rootExpression = `Buffer.from('${encodedRoot}','base64').toString('utf8')`;
|
||||
const resolveCommand = command => {
|
||||
// Leave invalid entries intact so managed-hook validation reports them.
|
||||
if (typeof command !== 'string') {
|
||||
return command;
|
||||
}
|
||||
const resolved = command
|
||||
.split(PLUGIN_ROOT_ENV_PROLOGUE)
|
||||
.join(`var e=${rootExpression};`);
|
||||
@@ -273,9 +277,11 @@ function resolveManagedHookCommands(managedHooks, targetRoot) {
|
||||
...entry,
|
||||
hooks: entry.hooks.map(hook => ({
|
||||
...hook,
|
||||
...(typeof hook.command === 'string'
|
||||
...(typeof hook.command === 'string' || Array.isArray(hook.command)
|
||||
? {
|
||||
command: resolveCommand(hook.command),
|
||||
command: Array.isArray(hook.command)
|
||||
? hook.command.map(resolveCommand)
|
||||
: resolveCommand(hook.command),
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
|
||||
@@ -373,9 +373,12 @@ async function applyPreflightedManagedPlan(entry) {
|
||||
: preflightManagedPlan(entry.preview.plan);
|
||||
const ownedDestinations = new Set(preview.ownershipSnapshot.destinations);
|
||||
let expectedStateFingerprint = preview.ownershipSnapshot.stateFingerprint;
|
||||
const expectedOperations = new Map(preview.operations.map(operation => [
|
||||
canonicalPath(operation.destinationPath), operation,
|
||||
// Several ordered JSON merges may share one destination. Preserve each
|
||||
// operation's preview instead of collapsing that sequence to one path entry.
|
||||
const expectedOperations = new Map(preview.plan.operations.map((operation, index) => [
|
||||
operation, preview.operations[index],
|
||||
]));
|
||||
const writtenDestinations = new Set();
|
||||
const assertStateUnchanged = () => (
|
||||
assertInstallStateUnchanged(preview.plan, expectedStateFingerprint)
|
||||
);
|
||||
@@ -385,12 +388,17 @@ async function applyPreflightedManagedPlan(entry) {
|
||||
};
|
||||
const assertOperationUnchanged = operation => {
|
||||
const destination = canonicalPath(operation.destinationPath);
|
||||
const expected = expectedOperations.get(destination);
|
||||
const expected = expectedOperations.get(operation);
|
||||
const currentClassification = classifyManagedOperation(operation, ownedDestinations);
|
||||
const expectedClassification = operation.kind === 'merge-json'
|
||||
&& writtenDestinations.has(destination)
|
||||
? 'managed-json-update'
|
||||
: expected && expected.classification;
|
||||
if (
|
||||
!expected
|
||||
|| expected.kind !== operation.kind
|
||||
|| expected.classification !== currentClassification
|
||||
|| canonicalPath(expected.destinationPath) !== destination
|
||||
|| expectedClassification !== currentClassification
|
||||
) {
|
||||
throw new Error(
|
||||
`Refusing to write ${operation.destinationPath}: destination changed after Kimi preflight.`
|
||||
@@ -412,6 +420,7 @@ async function applyPreflightedManagedPlan(entry) {
|
||||
assertStateUnchanged();
|
||||
const destination = assertOperationUnchanged(operation);
|
||||
ownedDestinations.add(destination);
|
||||
writtenDestinations.add(destination);
|
||||
},
|
||||
beforeInstallStateWrite: prepareInstallStateWrite,
|
||||
});
|
||||
|
||||
@@ -58,6 +58,21 @@ const START_PROCESS_SWITCH_PARAMETERS = new Set([
|
||||
'usenewenvironment',
|
||||
'wait',
|
||||
]);
|
||||
const ALIAS_VALUE_PARAMETERS = new Set([
|
||||
'name', 'value', 'description', 'option', 'scope',
|
||||
'erroraction', 'warningaction', 'informationaction', 'progressaction',
|
||||
'errorvariable', 'warningvariable', 'informationvariable',
|
||||
'outvariable', 'outbuffer', 'pipelinevariable',
|
||||
]);
|
||||
const ALIAS_SWITCH_PARAMETERS = new Set([
|
||||
'force', 'passthru', 'whatif', 'confirm', 'verbose', 'debug',
|
||||
]);
|
||||
const ALIAS_PARAMETER_ABBREVIATIONS = Object.freeze({
|
||||
ea: 'erroraction', wa: 'warningaction', infa: 'informationaction', proga: 'progressaction',
|
||||
ev: 'errorvariable', wv: 'warningvariable', iv: 'informationvariable',
|
||||
ov: 'outvariable', ob: 'outbuffer', pv: 'pipelinevariable',
|
||||
wi: 'whatif', cf: 'confirm', vb: 'verbose', db: 'debug',
|
||||
});
|
||||
const MAX_SCAN_DEPTH = 4;
|
||||
const MAX_CONTEXT_LENGTH = 4096;
|
||||
const DYNAMIC_EXECUTION_MARKER = '__ecc_dynamic_execution__';
|
||||
@@ -1225,16 +1240,22 @@ function addNestedScan(payload, depth, findings, analysis, options = {}, scanSta
|
||||
scanPowerShell(payload, depth + 1, findings, analysis, options, scanState);
|
||||
}
|
||||
|
||||
function staticPipelineInput(tokens) {
|
||||
function staticTokenValue(tokens, index, state, findings, inline = false) {
|
||||
const value = inline ? parameterValue(tokens[index]) : tokens[index];
|
||||
const quoteKind = inline ? tokens.inlineValueQuoteKinds?.[index] : tokens.quoteKinds?.[index];
|
||||
if (quoteKind === "'") return value;
|
||||
const source = inline
|
||||
? parameterValue(tokens.tokenSources?.[index] || tokens[index])
|
||||
: tokens.tokenSources?.[index] ?? value;
|
||||
return expandStaticDoubleQuotedString(source, state, findings);
|
||||
}
|
||||
|
||||
function staticPipelineInput(tokens, state, findings) {
|
||||
if (!tokens || tokens.length === 0) return null;
|
||||
if (tokens.length === 1) {
|
||||
const value = String(tokens[0] || '');
|
||||
return value || null;
|
||||
}
|
||||
if (tokens.length === 1) return staticTokenValue(tokens, 0, state, findings);
|
||||
const command = commandBasename(tokens[0]);
|
||||
if ((command === 'write-output' || command === 'echo') && tokens.length === 2) {
|
||||
const value = String(tokens[1] || '');
|
||||
return tokens.quotedTokens?.[1] === true || /\s/.test(value) ? value : null;
|
||||
return staticTokenValue(tokens, 1, state, findings);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1272,7 +1293,7 @@ function scanNestedPowerShell(tokens, depth, findings, analysis, scanState, upst
|
||||
let payload = inlinePayload
|
||||
? [inlinePayload, ...tokens.slice(index + 1)].join(' ')
|
||||
: tokens.slice(index + 1).join(' ');
|
||||
const pipelinePayload = payload === '-' ? staticPipelineInput(upstreamTokens) : null;
|
||||
const pipelinePayload = payload === '-' ? staticPipelineInput(upstreamTokens, scanState, findings) : null;
|
||||
const payloadIndex = index + 1;
|
||||
const inlineQuoteKind = tokens.inlineValueQuoteKinds?.[index];
|
||||
if (inlinePayload && inlineQuoteKind !== "'") {
|
||||
@@ -1754,26 +1775,70 @@ function scanScriptBlockConsumer(tokens, quotedTokens, findings, state) {
|
||||
}
|
||||
}
|
||||
|
||||
function staticAliasDefinition(tokens, quotedTokens = []) {
|
||||
let name = null;
|
||||
let value = null;
|
||||
function aliasParameterName(token) {
|
||||
const name = String(token).replace(/^-+/, '').split(':')[0].toLowerCase();
|
||||
if (Object.hasOwn(ALIAS_PARAMETER_ABBREVIATIONS, name)) return ALIAS_PARAMETER_ABBREVIATIONS[name];
|
||||
const parameters = [...ALIAS_VALUE_PARAMETERS, ...ALIAS_SWITCH_PARAMETERS];
|
||||
if (parameters.includes(name)) return name;
|
||||
const matches = parameters.filter(parameter => name && parameter.startsWith(name));
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
}
|
||||
|
||||
function aliasArguments(tokens, quotedTokens) {
|
||||
const named = new Map();
|
||||
const positional = [];
|
||||
let ambiguous = false;
|
||||
for (let index = 1; index < tokens.length; index += 1) {
|
||||
const token = tokens[index];
|
||||
if (!quotedTokens[index] && isParameterPrefix(token, 'name')) {
|
||||
name = parameterValue(token) || tokens[++index] || null;
|
||||
} else if (!quotedTokens[index] && isParameterPrefix(token, 'value')) {
|
||||
value = parameterValue(token) || tokens[++index] || null;
|
||||
} else if (!String(token).startsWith('-')) {
|
||||
positional.push(token);
|
||||
const token = String(tokens[index]);
|
||||
if (quotedTokens[index] || !token.startsWith('-')) {
|
||||
positional.push({ index, inline: false });
|
||||
if (!quotedTokens[index] && token.startsWith('@')) ambiguous = true;
|
||||
continue;
|
||||
}
|
||||
const parameter = aliasParameterName(token);
|
||||
if (!parameter) {
|
||||
ambiguous = true;
|
||||
continue;
|
||||
}
|
||||
if (named.has(parameter)) ambiguous = true;
|
||||
const inline = token.includes(':');
|
||||
const argument = { index: ALIAS_VALUE_PARAMETERS.has(parameter) && !inline ? ++index : index, inline };
|
||||
if (ALIAS_VALUE_PARAMETERS.has(parameter) && tokens[argument.index] === undefined) ambiguous = true;
|
||||
named.set(parameter, argument);
|
||||
// Option accepts a comma-separated array; its continuation belongs to the
|
||||
// named parameter rather than the remaining positional name/value slots.
|
||||
if (parameter === 'option') {
|
||||
while (index + 1 < tokens.length && !quotedTokens[index] &&
|
||||
(String(tokens[index]).endsWith(',') || String(tokens[index + 1]).startsWith(','))) index += 1;
|
||||
}
|
||||
}
|
||||
name ||= positional[0] || null;
|
||||
value ||= positional[1] || null;
|
||||
if (!/^[A-Za-z_][\w-]*$/.test(name || '') || !/^[A-Za-z_][\w./\\-]*$/.test(value || '')) {
|
||||
return null;
|
||||
}
|
||||
return { name: name.toLowerCase(), value };
|
||||
return { named, positional, ambiguous };
|
||||
}
|
||||
|
||||
function staticAliasDefinitions(tokens, quotedTokens, state) {
|
||||
const args = aliasArguments(tokens, quotedTokens);
|
||||
// Definitions stay inert. Uncertain binding is gated only when a candidate
|
||||
// alias is invoked, without allowing auxiliary values to hide its target.
|
||||
const unresolved = new Set();
|
||||
const resolve = argument => argument
|
||||
? staticTokenValue(tokens, argument.index, state, unresolved, argument.inline)
|
||||
: null;
|
||||
let positionalIndex = 0;
|
||||
const nameArgument = args.named.get('name') || args.positional[positionalIndex++];
|
||||
const valueArgument = args.named.get('value') || args.positional[positionalIndex++];
|
||||
const name = resolve(nameArgument);
|
||||
const value = resolve(valueArgument);
|
||||
const ambiguous = args.ambiguous || positionalIndex < args.positional.length;
|
||||
const possibleNames = args.named.has('value') ? args.positional : args.positional.slice(0, -1);
|
||||
const names = ambiguous && !args.named.has('name')
|
||||
? [name, ...possibleNames.map(resolve)]
|
||||
: [name];
|
||||
const target = ambiguous || value === null || /^@/.test(value)
|
||||
? DYNAMIC_EXECUTION_MARKER
|
||||
: value;
|
||||
if (!/^[A-Za-z_][\w./\\-]*$/.test(target || '')) return [];
|
||||
return [...new Set(names.filter(candidate => /^[A-Za-z_][\w-]*$/.test(candidate || '')))]
|
||||
.map(candidate => ({ name: candidate.toLowerCase(), value: target }));
|
||||
}
|
||||
|
||||
function scanInvokeScriptCalls(source, unquoted, depth, findings, analysis, state) {
|
||||
@@ -1862,9 +1927,10 @@ function scanPowerShell(command, depth, findings, analysis = null, options = {},
|
||||
state
|
||||
);
|
||||
}
|
||||
if (commandName === 'set-alias' || commandName === 'new-alias') {
|
||||
const definition = staticAliasDefinition(tokens, executable.quotedTokens);
|
||||
if (definition) state.aliases.set(definition.name, definition.value);
|
||||
if (['set-alias', 'new-alias', 'sal', 'nal'].includes(commandName)) {
|
||||
for (const definition of staticAliasDefinitions(tokens, executable.quotedTokens, state)) {
|
||||
state.aliases.set(definition.name, definition.value);
|
||||
}
|
||||
}
|
||||
const classInvocation = commandName.match(/^\[([a-z_][\w-]*)\]::/i);
|
||||
if (classInvocation) recordInvocation(state, `__class__:${classInvocation[1].toLowerCase()}`);
|
||||
|
||||
Reference in New Issue
Block a user