ubon
Version:
Security scanner for AI-generated apps (Cursor, Lovable, Windsurf, v0). Catches hardcoded secrets, prompt injection, hallucinated imports, Server Actions / Edge runtime mistakes, and the vibe-coded vulnerabilities traditional linters miss.
178 lines • 6.02 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.accessibilityRules = void 0;
const A11Y001_1 = __importDefault(require("./A11Y001"));
const A11Y002_1 = __importDefault(require("./A11Y002"));
const a11yFileTypes = ['jsx', 'tsx', 'svelte', 'astro', 'html'];
const A11Y003 = {
meta: {
id: 'A11Y003',
category: 'accessibility',
severity: 'high',
message: 'Empty button without aria-label',
fix: 'Add descriptive text or aria-label to buttons',
helpUri: 'https://dequeuniversity.com/rules/axe/4.7/button-name',
impact: 'Screen readers cannot announce button purpose, blocking critical actions'
},
impl: {
patterns: [
{
ruleId: 'A11Y003',
confidence: 0.85,
pattern: /<button(?![^>]*aria-label)>\s*<\/button>/gi,
message: 'Empty button without aria-label',
severity: 'high',
fix: 'Add descriptive text or aria-label to buttons'
}
],
fileTypes: a11yFileTypes
}
};
const A11Y004 = {
meta: {
id: 'A11Y004',
category: 'accessibility',
severity: 'medium',
message: 'Div with onClick (not keyboard accessible)',
fix: 'Use button element or add keyboard event handlers',
helpUri: 'https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Roles/button_role',
impact: 'Keyboard-only users cannot activate this interactive element'
},
impl: {
detect: (content, file, lines) => {
const results = [];
lines.forEach((line, index) => {
if (!/<div[^>]*(onClick|@click|v-on:click)/i.test(line))
return;
const hasRole = /role\s*=\s*['"]button['"]/i.test(line);
const hasTabIndex = /tabIndex\s*=\s*\{?0\}?/i.test(line);
if (hasRole && hasTabIndex)
return;
results.push({
line: index + 1,
match: line.slice(0, 200),
confidence: 0.75,
fixEdits: [{
file,
startLine: index + 1,
startColumn: 1,
endLine: index + 1,
endColumn: Math.max(1, line.length),
replacement: line.replace(/<div([^>]*)/i, (m0) => {
if (/role=/i.test(m0))
return m0;
return `${m0} role="button" tabIndex={0}`;
})
}]
});
});
return results;
},
fileTypes: a11yFileTypes
}
};
const A11Y005 = {
meta: {
id: 'A11Y005',
category: 'accessibility',
severity: 'low',
message: 'Link without href attribute',
fix: 'Add href attribute or use button element',
helpUri: 'https://dequeuniversity.com/rules/axe/4.7/link-name'
},
impl: {
patterns: [
{
ruleId: 'A11Y005',
confidence: 0.6,
pattern: /<a(?![^>]*href)/gi,
message: 'Link without href attribute',
severity: 'low',
fix: 'Add href attribute or use button element'
}
],
fileTypes: a11yFileTypes
}
};
const A11Y006 = {
meta: {
id: 'A11Y006',
category: 'accessibility',
severity: 'low',
message: 'Image missing width/height attributes',
fix: 'Specify width and height to avoid layout shifts',
helpUri: 'https://web.dev/optimize-cls/'
},
impl: {
patterns: [
{
ruleId: 'A11Y006',
confidence: 0.6,
pattern: /<img(?![^>]*\b(width|height)\s*=)/gi,
message: 'Image missing width/height attributes',
severity: 'low',
fix: 'Specify width and height to avoid layout shifts'
}
],
fileTypes: a11yFileTypes
}
};
const A11Y007 = {
meta: {
id: 'A11Y007',
category: 'accessibility',
severity: 'low',
message: 'next/image used without width and height',
fix: 'Provide width and height props to <Image>',
helpUri: 'https://nextjs.org/docs/pages/api-reference/components/image'
},
impl: {
patterns: [
{
ruleId: 'A11Y007',
confidence: 0.6,
pattern: /<Image\b(?![^>]*\b(width|height)\s*=)/gi,
message: 'next/image used without width and height',
severity: 'low',
fix: 'Provide width and height props to <Image>'
}
],
fileTypes: a11yFileTypes
}
};
const NEXT005 = {
meta: {
id: 'NEXT005',
category: 'accessibility',
severity: 'low',
message: 'External <img> used in Next.js app (consider next/image)',
fix: 'Use next/image for external sources with proper config'
},
impl: {
patterns: [
{
ruleId: 'NEXT005',
confidence: 0.6,
pattern: /<img[^>]*src\s*=\s*["']https?:\/\//gi,
message: 'External <img> used in Next.js app (consider next/image)',
severity: 'low',
fix: 'Use next/image for external sources with proper config'
}
],
fileTypes: a11yFileTypes
}
};
exports.accessibilityRules = {
A11Y001: A11Y001_1.default,
A11Y002: A11Y002_1.default,
A11Y003,
A11Y004,
A11Y005,
A11Y006,
A11Y007,
NEXT005
};
//# sourceMappingURL=index.js.map