Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Static Application Security Testing patterns, OWASP Top 10 checklist, language-specific vulnerability patterns, Semgrep rule writing guide, and CI/CD integration. Use when scanning code for security vulnerabilities or writing custom SAST rules.
.claude/skills/sast-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-15 | ✗→✓ | ▲ Improved | 212% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 214% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 306% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 320% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 137% | 0% |
Comprehensive vulnerability pattern library for static application security testing. Covers OWASP Top 10, language-specific patterns, Semgrep custom rules, and CI/CD pipeline integration.
What to Look For:
Detection Patterns:
javascript// VULNERABLE: No authorization check app.get('/api/users/:id', async (req, res) => { const user = await db.users.findById(req.params.id) res.json(user) // Anyone can access any user }) // SECURE: Authorization verified app.get('/api/users/:id', authenticate, async (req, res) => { if (req.user.id !== req.params.id && !req.user.isAdmin) { return res.status(403).json({ error: 'Forbidden' }) } const user = await db.users.findById(req.params.id) res.json(user) })
python# VULNERABLE: No permission check @app.route('/admin/delete/<user_id>', methods=['DELETE']) def delete_user(user_id): db.session.delete(User.query.get(user_id)) db.session.commit() # SECURE: Permission verified @app.route('/admin/delete/<user_id>', methods=['DELETE']) @login_required @admin_required def delete_user(user_id): db.session.delete(User.query.get(user_id)) db.session.commit()
Semgrep Rules:
yamlrules: - id: missing-auth-check patterns: - pattern: | app.$METHOD($PATH, async (req, res) => { ... $DB.$QUERY(...) ... }) - pattern-not: | app.$METHOD($PATH, authenticate, ...) message: "Endpoint missing authentication middleware" severity: ERROR
What to Look For:
Detection Patterns:
javascript// VULNERABLE: Weak password hashing const hash = crypto.createHash('md5').update(password).digest('hex') // SECURE: Strong password hashing const hash = await bcrypt.hash(password, 12)
python# VULNERABLE: Hardcoded secret SECRET_KEY = "my-super-secret-key-123" # SECURE: Environment variable SECRET_KEY = os.environ.get('SECRET_KEY') if not SECRET_KEY: raise ValueError("SECRET_KEY environment variable required")
Regex Patterns for Detection:
# API Keys
(?:api[_-]?key|apikey)\s*[:=]\s*['"][A-Za-z0-9_\-]{20,}['"]
# AWS Keys
(?:AKIA|ASIA)[A-Z0-9]{16}
# Generic Secrets
(?:password|passwd|pwd|secret|token)\s*[:=]\s*['"][^'"]{8,}['"]
# Private Keys
-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----
# JWT Tokens
eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+What to Look For:
Detection Patterns:
javascript// SQL Injection const query = `SELECT * FROM users WHERE id = ${userId}` // VULNERABLE const query = 'SELECT * FROM users WHERE id = $1' // SECURE // Command Injection exec(`ping ${hostname}`) // VULNERABLE execFile('ping', [hostname]) // SECURE // NoSQL Injection (MongoDB) db.users.find({ email: req.body.email }) // VULNERABLE if email = {"$gt": ""} db.users.find({ email: String(req.body.email) }) // SECURE: type coercion
python# SQL Injection cursor.execute(f"SELECT * FROM users WHERE id = {user_id}") # VULNERABLE cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) # SECURE # Command Injection os.system(f"convert {filename} output.png") # VULNERABLE subprocess.run(['convert', filename, 'output.png'], check=True) # SECURE
go// SQL Injection db.Query("SELECT * FROM users WHERE id = " + userID) // VULNERABLE db.Query("SELECT * FROM users WHERE id = $1", userID) // SECURE // Command Injection exec.Command("sh", "-c", userInput) // VULNERABLE exec.Command("ping", "-c", "1", hostname) // SECURE
java// SQL Injection stmt.executeQuery("SELECT * FROM users WHERE id = " + id); // VULNERABLE PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); ps.setString(1, id); // SECURE
What to Look For:
Detection Patterns:
javascript// VULNERABLE: No rate limiting on login app.post('/login', async (req, res) => { const user = await authenticate(req.body) res.json({ token: generateToken(user) }) }) // SECURE: Rate limited login const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, message: 'Too many login attempts' }) app.post('/login', loginLimiter, async (req, res) => { const user = await authenticate(req.body) res.json({ token: generateToken(user) }) })
python# VULNERABLE: Race condition in balance check balance = get_balance(user_id) if balance >= amount: withdraw(user_id, amount) # Another request could drain first # SECURE: Atomic transaction with db.transaction(): balance = db.query("SELECT balance FROM accounts WHERE id = %s FOR UPDATE", user_id) if balance >= amount: db.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", amount, user_id)
What to Look For:
Detection Patterns:
javascript// VULNERABLE: Debug mode app.set('env', 'development') // In production config // VULNERABLE: Missing security headers // No helmet or manual headers // SECURE: Security headers import helmet from 'helmet' app.use(helmet()) app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], } }))
python# VULNERABLE: Debug in production DEBUG = True # In production settings # VULNERABLE: Default secret key SECRET_KEY = 'django-insecure-change-me' # SECURE DEBUG = os.environ.get('DEBUG', 'False') == 'True' SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
Security Headers Checklist:
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()Detection Commands:
bash# Node.js npm audit npm audit --json npm outdated # Python pip-audit safety check pip list --outdated # Go go list -m -u all govulncheck ./... # Java mvn dependency-check:check gradle dependencyCheckAnalyze # Ruby bundle audit check
What to Look For:
Detection Patterns:
javascript// VULNERABLE: Plaintext password comparison if (password === user.password) { /* login */ } // SECURE: Hashed comparison const isValid = await bcrypt.compare(password, user.passwordHash) // VULNERABLE: Weak session management app.use(session({ secret: 'secret' })) // SECURE: Strong session app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, sameSite: 'strict', maxAge: 3600000 } }))
What to Look For:
Detection Patterns:
html<!-- VULNERABLE: No SRI --> <script src="https://cdn.example.com/lib.js"></script> <!-- SECURE: With SRI --> <script src="https://cdn.example.com/lib.js" integrity="sha384-abc123..." crossorigin="anonymous"></script>
python# VULNERABLE: Insecure deserialization import pickle data = pickle.loads(user_input) # SECURE: Use JSON import json data = json.loads(user_input)
What to Look For:
Detection Patterns:
javascript// VULNERABLE: No logging app.post('/login', async (req, res) => { const user = await authenticate(req.body) if (!user) return res.status(401).json({ error: 'Invalid' }) res.json({ token: generateToken(user) }) }) // SECURE: Audit logging app.post('/login', async (req, res) => { const user = await authenticate(req.body) if (!user) { logger.warn('Failed login attempt', { email: req.body.email, ip: req.ip, timestamp: new Date().toISOString() }) return res.status(401).json({ error: 'Invalid credentials' }) } logger.info('Successful login', { userId: user.id, ip: req.ip }) res.json({ token: generateToken(user) }) }) // VULNERABLE: Sensitive data in logs console.log('Login:', { email, password, token }) // SECURE: Redacted logs console.log('Login:', { email, passwordProvided: !!password })
What to Look For:
Detection Patterns:
javascript// VULNERABLE: SSRF const response = await fetch(req.query.url) // SECURE: URL whitelist const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'] const url = new URL(req.query.url) if (!ALLOWED_DOMAINS.includes(url.hostname)) { throw new Error('Domain not allowed') } // Also block internal IPs const ip = await dns.resolve(url.hostname) if (isPrivateIP(ip)) { throw new Error('Internal addresses not allowed') } const response = await fetch(url.toString())
python# VULNERABLE: SSRF response = requests.get(user_url) # SECURE: URL validation from urllib.parse import urlparse parsed = urlparse(user_url) if parsed.hostname not in ALLOWED_HOSTS: raise ValueError("Domain not allowed") if is_private_ip(socket.gethostbyname(parsed.hostname)): raise ValueError("Internal addresses blocked") response = requests.get(user_url, allow_redirects=False)
yamlrules: - id: rule-unique-id pattern: | eval($USER_INPUT) message: "eval() with dynamic input detected - potential RCE" languages: [javascript, typescript] severity: ERROR metadata: cwe: - CWE-94 owasp: - A03:2021 category: security confidence: HIGH
yaml# pattern: match exactly - pattern: eval($X) # pattern-not: exclude matches - pattern-not: eval("static-string") # patterns: AND (all must match) - patterns: - pattern: $DB.query($SQL) - pattern-not: $DB.query($SQL, $PARAMS) # pattern-either: OR (any can match) - pattern-either: - pattern: eval($X) - pattern: new Function($X) # pattern-inside: match within a context - pattern-inside: | app.$METHOD($PATH, (req, res) => { ... }) # pattern-not-inside: exclude context - pattern-not-inside: | app.$METHOD($PATH, authenticate, ...) # pattern-regex: regex in code - pattern-regex: "password\s*=\s*['\"][^'\"]{3,}['\"]"
yamlrules: - id: weak-hash-for-passwords patterns: - pattern: crypto.createHash($ALG).update($INPUT) - metavariable-regex: metavariable: $ALG regex: "('md5'|'sha1')" - metavariable-regex: metavariable: $INPUT regex: ".*password.*" message: "Weak hash algorithm used for password: $ALG" languages: [javascript, typescript] severity: ERROR
yamlrules: - id: sql-injection-taint mode: taint pattern-sources: - pattern: req.body.$PARAM - pattern: req.query.$PARAM - pattern: req.params.$PARAM pattern-sinks: - pattern: $DB.query($SQL) pattern-sanitizers: - pattern: $DB.escape($X) - pattern: sanitize($X) message: "User input flows to SQL query without sanitization" languages: [javascript] severity: ERROR
yamlrules: - id: no-hardcoded-secrets pattern-regex: | (?:api[_-]?key|secret|password|token)\s*[:=]\s*['"][A-Za-z0-9+/=_\-]{16,}['"] paths: exclude: - "*.test.*" - "*.spec.*" - "__tests__/*" - "*.example" message: "Potential hardcoded secret detected" languages: [generic] severity: ERROR - id: no-console-log-sensitive patterns: - pattern: console.log(..., $DATA, ...) - metavariable-regex: metavariable: $DATA regex: ".*(?:password|secret|token|key|credential).*" message: "Sensitive data in console.log" languages: [javascript, typescript] severity: WARNING - id: require-input-validation patterns: - pattern: | app.post($PATH, async (req, res) => { ... $DB.$METHOD(req.body) ... }) - pattern-not: | app.post($PATH, async (req, res) => { ... $SCHEMA.parse(...) ... }) message: "POST endpoint without input validation" languages: [javascript, typescript] severity: WARNING
yamlname: SAST Security Scan on: pull_request: branches: [main, develop] push: branches: [main] jobs: sast: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Semgrep Scan uses: semgrep/semgrep-action@v1 with: config: >- p/owasp-top-ten p/secrets p/typescript generateSarif: "1" env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 with: sarif_file: semgrep.sarif if: always() - name: Dependency Audit run: npm audit --audit-level=high - name: Check for Secrets uses: trufflesecurity/trufflehog@main with: extra_args: --only-verified
yaml# .pre-commit-config.yaml repos: - repo: https://github.com/semgrep/semgrep rev: 'v1.60.0' hooks: - id: semgrep args: ['--config', 'auto', '--error', '--severity', 'ERROR']
yamlsast: stage: test image: semgrep/semgrep script: - semgrep --config auto --config "p/secrets" --json --output gl-sast-report.json . artifacts: reports: sast: gl-sast-report.json rules: - if: $CI_MERGE_REQUEST_ID
bash# Full scan with auto rules semgrep --config auto . # OWASP + Secrets semgrep --config "p/owasp-top-ten" --config "p/secrets" . # Only errors (for CI gates) semgrep --config auto --error --severity ERROR . # JSON output for processing semgrep --config auto --json --output report.json . # Scan specific files semgrep --config auto src/api/ src/auth/ # Diff-aware (only changed code) semgrep --config auto --baseline-commit origin/main # Custom rules semgrep --config .semgrep.yml . # Exclude test files semgrep --config auto --exclude="*test*" --exclude="*spec*" . # With metrics disabled (privacy) semgrep --config auto --metrics=off .
| Severity | Criteria | Action | SLA | |----------|----------|--------|-----| | CRITICAL | Exploitable RCE, SQLi, auth bypass, data breach | Fix IMMEDIATELY, block deploy | < 1 hour | | HIGH | XSS, SSRF, IDOR, missing auth, weak crypto | Fix before production | < 24 hours | | MEDIUM | Missing validation, verbose errors, weak headers | Fix in current sprint | < 1 week | | LOW | Debug mode, outdated lib (no known CVE), info leak | Fix in backlog | < 1 month |
Is user input involved?
YES -> Does it reach a dangerous sink (DB, exec, DOM)?
YES -> Is it sanitized/validated?
NO -> CRITICAL (injection)
YES -> Check sanitizer adequacy -> MEDIUM if weak
NO -> MEDIUM (missing validation)
NO -> Is it a configuration issue?
YES -> Affects security posture?
YES -> HIGH (misconfiguration)
NO -> LOW (best practice)
NO -> Is it a dependency issue?
YES -> Known CVE?
YES -> Match CVE severity
NO -> LOW (outdated)
NO -> InformationalRemember: SAST catches patterns, not intent. Always verify findings with manual review. A finding is only a vulnerability if it can be exploited in the application's specific context.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 17,750 | 9,888 | -44% | 1 | 1 | 0% | 2,947 | 7,361 | +150% | 0 | 0 | — |
case-02 | pass→pass | 19,626 | 13,375 | -32% | 1 | 1 | 0% | 1,699 | 7,139 | +320% | 0 | 0 | — |
case-03 | pass→pass | 23,532 | 28,233 | +20% | 1 | 1 | 0% | 4,159 | 9,872 | +137% | 0 | 0 | — |
case-04 | pass→pass | 13,718 | 15,492 | +13% | 1 | 1 | 0% | 2,820 | 8,512 | +202% | 0 | 0 | — |
case-05 | pass→pass | 14,259 | 8,981 | -37% | 1 | 1 | 0% | 2,738 | 7,074 | +158% | 0 | 0 | — |
case-06 | pass→pass | 10,048 | 9,533 | -5% | 1 | 1 | 0% | 2,138 | 7,172 | +235% | 0 | 0 | — |
case-07 | pass→pass | 7,840 | 6,843 | -13% | 1 | 1 | 0% | 1,512 | 6,569 | +334% | 0 | 0 | — |
case-08 | pass→pass | 10,708 | 7,716 | -28% | 1 | 1 | 0% | 1,783 | 6,934 | +289% | 0 | 0 | — |
case-09 | pass→pass | 11,672 | 6,701 | -43% | 1 | 1 | 0% | 2,199 | 6,641 | +202% | 0 | 0 | — |
case-10 | pass→pass | 13,108 | 8,003 | -39% | 1 | 1 | 0% | 2,726 | 7,044 | +158% | 0 | 0 | — |
case-11 | pass→pass | 11,944 | 11,749 | -2% | 1 | 1 | 0% | 2,421 | 7,703 | +218% | 0 | 0 | — |
case-12 | pass→pass | 7,293 | 6,503 | -11% | 1 | 1 | 0% | 1,559 | 6,606 | +324% | 0 | 0 | — |
case-13 | pass→pass | 8,526 | 6,949 | -18% | 1 | 1 | 0% | 1,587 | 6,587 | +315% | 0 | 0 | — |
case-14 | pass→pass | 13,015 | 10,789 | -17% | 1 | 1 | 0% | 2,426 | 7,510 | +210% | 0 | 0 | — |
case-15 | fail→pass | 10,452 | 3,064 | -71% | 1 | 1 | 0% | 1,854 | 5,787 | +212% | 0 | 0 | — |
case-16 | pass→pass | 3,786 | 3,382 | -11% | 1 | 1 | 0% | 666 | 5,866 | +781% | 0 | 0 | — |
case-17 | fail→pass | 10,538 | 3,333 | -68% | 1 | 1 | 0% | 1,924 | 6,037 | +214% | 0 | 0 | — |
case-18 | pass→pass | 4,479 | 7,131 | +59% | 1 | 1 | 0% | 736 | 6,546 | +789% | 0 | 0 | — |
case-19 | fail→pass | 8,747 | 7,075 | -19% | 1 | 1 | 0% | 1,645 | 6,681 | +306% | 0 | 0 | — |
case-20 | pass→pass | 8,920 | 3,949 | -56% | 1 | 1 | 0% | 1,665 | 5,951 | +257% | 0 | 0 | — |
case-21 | pass→pass | 5,429 | 2,699 | -50% | 1 | 1 | 0% | 1,037 | 5,887 | +468% | 0 | 0 | — |
case-22 | pass→pass | 12,243 | 8,068 | -34% | 1 | 1 | 0% | 2,261 | 6,822 | +202% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 7/29/2026 | +26% |
Other measured skills in the registry, with their headline benchmark lift.