fix: 强化NapCat审计只读与路径校验
This commit is contained in:
parent
1f60506c89
commit
7619dd5928
@ -129,8 +129,8 @@ export async function runSelfTest(): Promise<void> {
|
|||||||
const dryRunAuditCommandText = dryRunAuditCommands.join("\n");
|
const dryRunAuditCommandText = dryRunAuditCommands.join("\n");
|
||||||
for (const expected of [
|
for (const expected of [
|
||||||
"git status --short --branch",
|
"git status --short --branch",
|
||||||
"git fetch upstream --tags --prune",
|
"git fetch --dry-run upstream --tags --prune",
|
||||||
"git fetch origin --prune",
|
"git fetch --dry-run origin --prune",
|
||||||
"git diff --name-only",
|
"git diff --name-only",
|
||||||
"git merge-tree",
|
"git merge-tree",
|
||||||
]) {
|
]) {
|
||||||
@ -140,6 +140,17 @@ export async function runSelfTest(): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const command of dryRunAudit.commands) {
|
||||||
|
if (
|
||||||
|
command.readOnly &&
|
||||||
|
/^git\s+fetch(?:\s|$)/.test(command.command) &&
|
||||||
|
!/\s--dry-run(?:\s|$)/.test(command.command)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"NapCat upstream audit read-only fetch must use --dry-run",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const forbidden of [
|
for (const forbidden of [
|
||||||
{ label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ },
|
{ label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ },
|
||||||
{ label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ },
|
{ label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ },
|
||||||
@ -153,6 +164,50 @@ export async function runSelfTest(): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const invalidPathCase of [
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "Windows absolute path",
|
||||||
|
upstreamChangedFiles: ["D:/repo/packages/napcat-core/login/runtime.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "Unix absolute path",
|
||||||
|
upstreamChangedFiles: ["/repo/packages/napcat-core/login/runtime.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "UNC path",
|
||||||
|
upstreamChangedFiles: [
|
||||||
|
"\\\\server\\share\\packages\\napcat-core\\login\\runtime.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: [],
|
||||||
|
name: "parent segment path",
|
||||||
|
upstreamChangedFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forkPatchFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||||||
|
name: "fork patch parent segment path",
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const invalidPathAudit = classifyNapcatUpstreamAudit({
|
||||||
|
dryMergeConflict: false,
|
||||||
|
forkPatchFiles: invalidPathCase.forkPatchFiles,
|
||||||
|
upstreamChangedFiles: invalidPathCase.upstreamChangedFiles,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
invalidPathAudit.classification !== "blocked" ||
|
||||||
|
invalidPathAudit.recommendedAction !== "block" ||
|
||||||
|
!invalidPathAudit.reasonCodes.includes("METADATA_UNAVAILABLE")
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`NapCat invalid audit path self-check failed: ${invalidPathCase.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const promptName of napcatAutomationPromptNames) {
|
for (const promptName of napcatAutomationPromptNames) {
|
||||||
const promptText = loadNapcatAutomationPrompt(promptName);
|
const promptText = loadNapcatAutomationPrompt(promptName);
|
||||||
if (!promptText.includes("Do not edit files")) {
|
if (!promptText.includes("Do not edit files")) {
|
||||||
|
|||||||
@ -75,6 +75,46 @@ const hotZoneMatchers: Array<{ pattern: string; regex: RegExp }> = [
|
|||||||
{ pattern: 'vite*.ts', regex: /(?:^|\/)vite[^/]*\.ts$/i },
|
{ pattern: 'vite*.ts', regex: /(?:^|\/)vite[^/]*\.ts$/i },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether Git audit metadata contains a repo-relative path that the classifier can trust.
|
||||||
|
* @param value - Raw path string from fork/upstream Git diff collectors; empty entries are allowed because collectors may emit blank lines.
|
||||||
|
* @returns True when a non-empty path is absolute, UNC-based, or contains a parent-directory segment.
|
||||||
|
*/
|
||||||
|
function hasInvalidAuditPath(value: string): boolean {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
|
||||||
|
if (trimmed.startsWith('\\\\') || trimmed.startsWith('//')) return true;
|
||||||
|
if (trimmed.startsWith('/')) return true;
|
||||||
|
|
||||||
|
const normalized = trimmed
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^\.\/+/, '')
|
||||||
|
.replace(/\/+/g, '/');
|
||||||
|
return normalized.split('/').includes('..');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks both file lists for invalid non repo-relative metadata.
|
||||||
|
* @param input - Collector result carrying fork patch and upstream changed file paths; both arrays must contain repo-relative Git paths when non-empty.
|
||||||
|
* @returns True when any path entry would make deterministic hot-zone and overlap classification untrustworthy.
|
||||||
|
*/
|
||||||
|
function hasInvalidAuditMetadata(
|
||||||
|
input: NapcatAuditClassificationInput,
|
||||||
|
): boolean {
|
||||||
|
for (const file of input.forkPatchFiles) {
|
||||||
|
if (hasInvalidAuditPath(file)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const file of input.upstreamChangedFiles) {
|
||||||
|
if (hasInvalidAuditPath(file)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes Git path output into a stable comparison key for audit classifiers.
|
* Normalizes Git path output into a stable comparison key for audit classifiers.
|
||||||
* @param value - File path from Git diff/status collectors; may contain Windows separators, leading `./`, or surrounding whitespace, but must represent a repo-relative path.
|
* @param value - File path from Git diff/status collectors; may contain Windows separators, leading `./`, or surrounding whitespace, but must represent a repo-relative path.
|
||||||
@ -195,6 +235,16 @@ function buildClassificationSummary(
|
|||||||
export function classifyNapcatUpstreamAudit(
|
export function classifyNapcatUpstreamAudit(
|
||||||
input: NapcatAuditClassificationInput,
|
input: NapcatAuditClassificationInput,
|
||||||
): NapcatAuditClassificationResult {
|
): NapcatAuditClassificationResult {
|
||||||
|
if (hasInvalidAuditMetadata(input)) {
|
||||||
|
return {
|
||||||
|
classification: 'blocked',
|
||||||
|
reasonCodes: ['METADATA_UNAVAILABLE'],
|
||||||
|
recommendedAction: 'block',
|
||||||
|
summary:
|
||||||
|
'Invalid audit metadata: fork and upstream file paths must be repo-relative Git paths without absolute roots, UNC prefixes, or parent-directory segments.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (input.dryMergeConflict) {
|
if (input.dryMergeConflict) {
|
||||||
return {
|
return {
|
||||||
classification: 'blocked',
|
classification: 'blocked',
|
||||||
@ -254,13 +304,15 @@ export function buildNapcatUpstreamAudit(
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: 'git fetch upstream --tags --prune',
|
command: 'git fetch --dry-run upstream --tags --prune',
|
||||||
description: 'Refresh upstream release refs before file-level audit collection.',
|
description:
|
||||||
|
'Probe upstream release ref availability without updating local refs.',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: 'git fetch origin --prune',
|
command: 'git fetch --dry-run origin --prune',
|
||||||
description: 'Refresh fork refs before comparing KT patch and upstream ranges.',
|
description:
|
||||||
|
'Probe fork ref availability without updating remote-tracking refs.',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user