mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Report unreadable detector directories
AI assistance disclosure: Codex implemented and tested this fix under maintainer direction.
This commit is contained in:
@@ -401,7 +401,7 @@ async function detectCli() {
|
||||
}
|
||||
}
|
||||
|
||||
const files = walkDir(resolved)
|
||||
const files = walkDir(resolved, reportLocalScanFailure)
|
||||
.filter(file => !shouldIgnoreDetectionFile(file, process.cwd(), detectionConfig));
|
||||
const htmlCount = files.filter(f => HTML_EXTENSIONS.has(path.extname(f).toLowerCase())).length;
|
||||
|
||||
|
||||
@@ -46,15 +46,20 @@ const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/@(?:use|forward)\s+['"]([^'"]+)['"]/g,
|
||||
];
|
||||
|
||||
function walkDir(dir) {
|
||||
function walkDir(dir, onReadError = null) {
|
||||
const files = [];
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return files; }
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if (onReadError) onReadError(dir, error);
|
||||
return files;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name)) continue;
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
if (entry.isDirectory()) files.push(...walkDir(full, onReadError));
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
|
||||
@@ -2648,6 +2648,21 @@ describe('walkDir', () => {
|
||||
expect(walkDir('/nonexistent/path/12345')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('reports directory traversal errors to an optional handler', () => {
|
||||
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-walk-error-'));
|
||||
try {
|
||||
const missing = path.join(parent, 'missing');
|
||||
const errors = [];
|
||||
|
||||
expect(walkDir(missing, (dir, error) => {
|
||||
errors.push({ dir, code: error.code });
|
||||
})).toHaveLength(0);
|
||||
expect(errors).toEqual([{ dir: missing, code: 'ENOENT' }]);
|
||||
} finally {
|
||||
fs.rmSync(parent, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Issue #303: when impeccable (or any agent tool) is installed into a
|
||||
// project's .claude/.cursor/etc. tree, a root scan descended into the
|
||||
// vendored skill code and reported the detector's own example strings as
|
||||
|
||||
@@ -67,9 +67,10 @@ function runWithoutPuppeteer(args, files = {}, { nodeArgs = [] } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const DENY_READ_PRELOAD = `
|
||||
const DENY_FS_PRELOAD = `
|
||||
const fs = require('node:fs');
|
||||
const originalReadFileSync = fs.readFileSync;
|
||||
const originalReaddirSync = fs.readdirSync;
|
||||
fs.readFileSync = function (file, ...args) {
|
||||
if (String(file).endsWith('unreadable.css')) {
|
||||
const error = new Error('simulated EACCES');
|
||||
@@ -78,6 +79,14 @@ fs.readFileSync = function (file, ...args) {
|
||||
}
|
||||
return originalReadFileSync.call(this, file, ...args);
|
||||
};
|
||||
fs.readdirSync = function (dir, ...args) {
|
||||
if (String(dir).endsWith('unreadable-dir')) {
|
||||
const error = new Error('simulated directory EACCES');
|
||||
error.code = 'EACCES';
|
||||
throw error;
|
||||
}
|
||||
return originalReaddirSync.call(this, dir, ...args);
|
||||
};
|
||||
`;
|
||||
|
||||
describe('launchBrowser', () => {
|
||||
@@ -176,10 +185,10 @@ describe('detect CLI browser failures', () => {
|
||||
const result = runWithoutPuppeteer(
|
||||
['unreadable.css'],
|
||||
{
|
||||
'deny-read.cjs': DENY_READ_PRELOAD,
|
||||
'deny-fs.cjs': DENY_FS_PRELOAD,
|
||||
'unreadable.css': '.hero { color: red; }\n',
|
||||
},
|
||||
{ nodeArgs: ['--require=./deny-read.cjs'] },
|
||||
{ nodeArgs: ['--require=./deny-fs.cjs'] },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
@@ -192,11 +201,11 @@ describe('detect CLI browser failures', () => {
|
||||
const result = runWithoutPuppeteer(
|
||||
['styles'],
|
||||
{
|
||||
'deny-read.cjs': DENY_READ_PRELOAD,
|
||||
'deny-fs.cjs': DENY_FS_PRELOAD,
|
||||
'styles/unreadable.css': '.hero { color: red; }\n',
|
||||
'styles/page.css': '.hero { animation: bounce 1s linear infinite; }\n',
|
||||
},
|
||||
{ nodeArgs: ['--require=./deny-read.cjs'] },
|
||||
{ nodeArgs: ['--require=./deny-fs.cjs'] },
|
||||
);
|
||||
const findings = JSON.parse(result.stdout);
|
||||
|
||||
@@ -204,6 +213,39 @@ describe('detect CLI browser failures', () => {
|
||||
expect(findings.some(finding => finding.antipattern === 'bounce-easing')).toBe(true);
|
||||
expect(result.stderr.match(/simulated EACCES/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('unreadable local directory exits 1 with valid empty JSON', () => {
|
||||
const result = runWithoutPuppeteer(
|
||||
['unreadable-dir'],
|
||||
{
|
||||
'deny-fs.cjs': DENY_FS_PRELOAD,
|
||||
'unreadable-dir/page.css': '.hero { color: red; }\n',
|
||||
},
|
||||
{ nodeArgs: ['--require=./deny-fs.cjs'] },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe('[]\n');
|
||||
expect(result.stderr).toContain('Error: cannot scan');
|
||||
expect(result.stderr).toContain('unreadable-dir: simulated directory EACCES');
|
||||
});
|
||||
|
||||
test('unreadable nested directory preserves findings from readable siblings', () => {
|
||||
const result = runWithoutPuppeteer(
|
||||
['project'],
|
||||
{
|
||||
'deny-fs.cjs': DENY_FS_PRELOAD,
|
||||
'project/unreadable-dir/hidden.css': '.hero { color: red; }\n',
|
||||
'project/page.css': '.hero { animation: bounce 1s linear infinite; }\n',
|
||||
},
|
||||
{ nodeArgs: ['--require=./deny-fs.cjs'] },
|
||||
);
|
||||
const findings = JSON.parse(result.stdout);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(findings.some(finding => finding.antipattern === 'bounce-easing')).toBe(true);
|
||||
expect(result.stderr.match(/simulated directory EACCES/g)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitScanUrl', () => {
|
||||
|
||||
Reference in New Issue
Block a user