mirror of
https://github.com/semgrep/skills.git
synced 2026-09-20 14:21:26 +08:00
823e429b92
- Add llm-security skill covering OWASP Top 10 for LLM Applications 2025
- 10 rules: Prompt Injection, Sensitive Disclosure, Supply Chain,
Data Poisoning, Output Handling, Excessive Agency, System Prompt
Leakage, Vector/Embedding Weaknesses, Misinformation, Unbounded
Consumption
- Python code examples with vulnerable/secure patterns
- Rename packages/code-security-build to packages/skill-build
- Accept skill name as CLI argument: `pnpm validate llm-security`
- Auto-discover skills with rules/ directories
- Support Vulnerable/Secure labels (in addition to Incorrect/Correct)
- Update Makefile to build all skills automatically
- `make validate` - validates all skills
- `make build` - builds AGENTS.md for all skills
- `make validate-skill SKILL=name` - single skill operations
- Update READMEs with llm-security documentation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
155 lines
4.8 KiB
JavaScript
155 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Validate rule files follow the correct structure
|
|
*
|
|
* Usage: tsx src/validate.ts [skill-name]
|
|
* If no skill name provided, defaults to 'code-security'
|
|
*/
|
|
|
|
import { readdir } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { Rule } from './types.js'
|
|
import { parseRuleFile } from './parser.js'
|
|
import { RULES_DIR, SKILL_NAME, validateSkillExists } from './config.js'
|
|
|
|
interface ValidationError {
|
|
file: string
|
|
ruleId?: string
|
|
message: string
|
|
}
|
|
|
|
interface ValidationWarning {
|
|
file: string
|
|
message: string
|
|
}
|
|
|
|
interface ValidationResult {
|
|
errors: ValidationError[]
|
|
warnings: ValidationWarning[]
|
|
}
|
|
|
|
/**
|
|
* Validate a rule
|
|
*/
|
|
function validateRule(rule: Rule, file: string): ValidationResult {
|
|
const errors: ValidationError[] = []
|
|
const warnings: ValidationWarning[] = []
|
|
|
|
// Note: rule.id is auto-generated during build, not required in source files
|
|
|
|
if (!rule.title || rule.title.trim().length === 0) {
|
|
errors.push({ file, ruleId: rule.id, message: 'Missing or empty title' })
|
|
}
|
|
|
|
if (!rule.explanation || rule.explanation.trim().length === 0) {
|
|
errors.push({ file, ruleId: rule.id, message: 'Missing or empty explanation' })
|
|
}
|
|
|
|
if (!rule.examples || rule.examples.length === 0) {
|
|
errors.push({ file, ruleId: rule.id, message: 'Missing examples (need at least one bad and one good example)' })
|
|
} else {
|
|
// Filter out informational examples (notes, trade-offs, etc.) that don't have code
|
|
const codeExamples = rule.examples.filter(e => e.code && e.code.trim().length > 0)
|
|
|
|
const hasBad = codeExamples.some(e =>
|
|
e.label.toLowerCase().includes('incorrect') ||
|
|
e.label.toLowerCase().includes('wrong') ||
|
|
e.label.toLowerCase().includes('bad') ||
|
|
e.label.toLowerCase().includes('vulnerable') ||
|
|
e.label.toLowerCase().includes('insecure')
|
|
)
|
|
const hasGood = codeExamples.some(e =>
|
|
e.label.toLowerCase().includes('correct') ||
|
|
e.label.toLowerCase().includes('good') ||
|
|
e.label.toLowerCase().includes('usage') ||
|
|
e.label.toLowerCase().includes('implementation') ||
|
|
e.label.toLowerCase().includes('example') ||
|
|
e.label.toLowerCase().includes('secure') ||
|
|
e.label.toLowerCase().includes('safe')
|
|
)
|
|
|
|
if (codeExamples.length === 0) {
|
|
errors.push({ file, ruleId: rule.id, message: 'Missing code examples' })
|
|
} else if (!hasBad && !hasGood) {
|
|
errors.push({ file, ruleId: rule.id, message: 'Missing bad/incorrect or good/correct examples' })
|
|
}
|
|
}
|
|
|
|
const validImpacts: Rule['impact'][] = ['CRITICAL', 'HIGH', 'MEDIUM-HIGH', 'MEDIUM', 'LOW-MEDIUM', 'LOW']
|
|
if (!validImpacts.includes(rule.impact)) {
|
|
errors.push({ file, ruleId: rule.id, message: `Invalid impact level: ${rule.impact}. Must be one of: ${validImpacts.join(', ')}` })
|
|
}
|
|
|
|
// Warnings for missing optional fields
|
|
if (!rule.impactDescription) {
|
|
warnings.push({ file, message: 'Missing impactDescription field' })
|
|
}
|
|
|
|
if (!rule.tags || rule.tags.length === 0) {
|
|
warnings.push({ file, message: 'Missing tags field' })
|
|
}
|
|
|
|
return { errors, warnings }
|
|
}
|
|
|
|
/**
|
|
* Main validation function
|
|
*/
|
|
async function validate() {
|
|
try {
|
|
// Validate skill exists
|
|
validateSkillExists()
|
|
|
|
console.log(`Validating rule files for skill: ${SKILL_NAME}`)
|
|
console.log(`Rules directory: ${RULES_DIR}`)
|
|
|
|
const files = await readdir(RULES_DIR)
|
|
const ruleFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('_'))
|
|
|
|
const allErrors: ValidationError[] = []
|
|
const allWarnings: ValidationWarning[] = []
|
|
|
|
for (const file of ruleFiles) {
|
|
const filePath = join(RULES_DIR, file)
|
|
try {
|
|
const { rule } = await parseRuleFile(filePath)
|
|
const { errors, warnings } = validateRule(rule, file)
|
|
allErrors.push(...errors)
|
|
allWarnings.push(...warnings)
|
|
} catch (error) {
|
|
allErrors.push({
|
|
file,
|
|
message: `Failed to parse: ${error instanceof Error ? error.message : String(error)}`
|
|
})
|
|
}
|
|
}
|
|
|
|
// Report warnings (non-fatal)
|
|
if (allWarnings.length > 0) {
|
|
console.log(`\n⚠ Warnings (${allWarnings.length}):\n`)
|
|
allWarnings.forEach(warning => {
|
|
console.log(` ${warning.file}: ${warning.message}`)
|
|
})
|
|
}
|
|
|
|
// Report errors (fatal)
|
|
if (allErrors.length > 0) {
|
|
console.error('\n✗ Validation failed:\n')
|
|
allErrors.forEach(error => {
|
|
console.error(` ${error.file}${error.ruleId ? ` (${error.ruleId})` : ''}: ${error.message}`)
|
|
})
|
|
process.exit(1)
|
|
} else {
|
|
console.log(`\n✓ ${SKILL_NAME}: All ${ruleFiles.length} rule files are valid`)
|
|
if (allWarnings.length > 0) {
|
|
console.log(` (${allWarnings.length} warnings - consider adding missing optional fields)`)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Validation failed:', error)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
validate()
|