{"schemaVersion":1,"id":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules","title":"Automated Code Review: Semgrep Custom Rules, AST Pattern Matching, and Building Org-Specific Security Scanners","description":"A practical guide to automated code review with Semgrep, writing custom rules with AST pattern matching, taint analysis for tracking untrusted data, building organization-specific security scanners, and integrating everything into CI for automated enforcement.","language":"en","datePublished":"2026-01-15","dateModified":null,"contentFormat":"text/markdown","content":"Developer Tools\n\n# Automated Code Review: Semgrep Custom Rules, AST Pattern Matching, and Building Org-Specific Security Scanners\n\n2026-01-155 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/ai-code-review-ast-analysis-semgrep-custom-rules>)\n\n## Why Semgrep, Not Regex\n\nThe fundamental problem with grep-based security scanning is that code is a tree, not a string. [Semgrep](<https://semgrep.dev/>) addresses this by operating on the Abstract Syntax Tree. Consider finding SQL injection in [Python](<https://www.python.org/>):\n\n```python\n# Grep catches this:\ncursor.execute(\"SELECT * FROM users WHERE id = \" + user_id)\n\n# But misses this (same vulnerability, different formatting):\nquery = \"SELECT * FROM users WHERE id = \" + user_id\ncursor.execute(\n query\n)\n\n# And false-positives on this (safe, parameterized):\ncursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))\n```\n\nSemgrep works on the AST (Abstract Syntax Tree), so it understands code structure, not just text:\n\n```yaml\n# semgrep rule: python-sql-injection.yaml\nrules:\n - id: python-sql-injection\n patterns:\n - pattern: cursor.execute($QUERY, ...)\n - pattern-not: cursor.execute(\"...\", (...))\n - metavariable-pattern:\n metavariable: $QUERY\n patterns:\n - pattern: |\n $X + ...\n - pattern: |\n f\"...\"\n - pattern: |\n \"...\".format(...)\n - pattern: |\n \"...\" % ...\n message: >\n SQL query built with string concatenation or formatting.\n Use parameterized queries instead.\n severity: ERROR\n languages: [python]\n```\n\nThis matches any ` cursor.execute() ` where the query argument is built with concatenation, f-strings, ` .format() `, or ` % ` formatting, but explicitly excludes parameterized queries. It works regardless of whitespace, variable naming, or how many lines the code spans.\n\n## Writing Your First Custom Rule\n\nSemgrep rules are YAML files with a surprisingly powerful pattern language. The key building blocks:\n\n```yaml\nrules:\n - id: no-hardcoded-secrets\n patterns:\n # Match: any string assignment that looks like a secret\n - pattern: $KEY = \"...\"\n # Where the variable name suggests it's a secret\n - metavariable-regex:\n metavariable: $KEY\n regex: (?i)(password|secret|api_key|token|private_key)\n # But not when it's empty or a placeholder\n - pattern-not: $KEY = \"\"\n - pattern-not: $KEY = \"CHANGEME\"\n - pattern-not: $KEY = \"placeholder\"\n - pattern-not: $KEY = \"xxx\"\n message: |\n Hardcoded secret detected in variable '$KEY'.\n Use environment variables or a secrets manager instead.\n severity: WARNING\n languages: [python, javascript, typescript, java, go, ruby]\n metadata:\n category: security\n confidence: MEDIUM\n cwe: CWE-798\n```\n\nTest it:\n\n```bash\n# Create test file\ncat > test_secrets.py << 'EOF'\n# ruleid: no-hardcoded-secrets\napi_key = \"sk_live_4242424242424242\"\n\n# ruleid: no-hardcoded-secrets\nDB_PASSWORD = \"super_secret_123\"\n\n# ok: no-hardcoded-secrets\napi_key = \"\"\n\n# ok: no-hardcoded-secrets\npassword = os.environ.get(\"DB_PASSWORD\")\nEOF\n\n# Run the rule\nsemgrep --config python-secrets.yaml test_secrets.py\n```\n\n## Taint Analysis: Following the Data\n\nThis is where Semgrep goes beyond pattern matching. Taint mode tracks data flow from sources to sinks:\n\n```yaml\nrules:\n - id: express-xss\n mode: taint\n message: >\n User input flows into HTML response without sanitization.\n This is a Cross-Site Scripting (XSS) vulnerability.\n severity: ERROR\n languages: [javascript, typescript]\n metadata:\n cwe: CWE-79\n owasp: A7:2017\n\n pattern-sources:\n - patterns:\n - pattern: req.query.$PARAM\n - patterns:\n - pattern: req.body.$PARAM\n - patterns:\n - pattern: req.params.$PARAM\n\n pattern-sinks:\n - patterns:\n - pattern: res.send($PAYLOAD)\n - patterns:\n - pattern: res.write($PAYLOAD)\n\n pattern-sanitizers:\n - patterns:\n - pattern: DOMPurify.sanitize(...)\n - patterns:\n - pattern: escape(...)\n - patterns:\n - pattern: encodeURIComponent(...)\n```\n\nThis catches:\n\n```javascript\n// DETECTED: tainted data flows from source to sink\napp.get('/search', (req, res) => {\n const query = req.query.q;\n res.send(`<h1>Results for: ${query}</h1>`); // XSS!\n});\n\n// DETECTED: even through intermediate variables\napp.get('/profile', (req, res) => {\n const userId = req.params.id;\n const name = getUserName(userId);\n const html = `<div>${req.query.displayName}</div>`;\n res.send(html); // XSS via req.query.displayName\n});\n\n// NOT DETECTED: sanitizer breaks the taint chain\napp.get('/safe', (req, res) => {\n const query = DOMPurify.sanitize(req.query.q);\n res.send(`<h1>Results for: ${query}</h1>`); // Safe\n});\n```\n\n## Building an Org-Specific Rule Set\n\nBeyond security rules, the rule set can expand to organization-specific patterns:\n\n```yaml\n# Rule: Enforce our internal API client usage\nrules:\n - id: use-internal-http-client\n patterns:\n - pattern-either:\n - pattern: fetch(...)\n - pattern: axios.get(...)\n - pattern: axios.post(...)\n - pattern: new XMLHttpRequest()\n - pattern-not-inside: |\n // semgrep-ignore: use-internal-http-client\n ...\n paths:\n include:\n - \"src/**\"\n exclude:\n - \"src/lib/http-client/**\"\n - \"**/*.test.*\"\n message: |\n Use our internal HTTP client (`@myorg/http-client`) instead of\n raw fetch/axios. It handles auth, retries, circuit breaking,\n and distributed tracing automatically.\n severity: WARNING\n languages: [javascript, typescript]\n\n # Rule: Prevent direct database access outside repositories\n - id: no-direct-db-access\n patterns:\n - pattern-either:\n - pattern: prisma.$MODEL.$METHOD(...)\n - pattern: db.query(...)\n - pattern: knex($TABLE)\n - pattern-not-inside: |\n class $REPO {\n ...\n }\n paths:\n include:\n - \"src/**\"\n exclude:\n - \"src/repositories/**\"\n - \"src/migrations/**\"\n message: |\n Direct database access outside the repository layer.\n All database operations should go through repository classes.\n severity: WARNING\n languages: [javascript, typescript]\n```\n\n## CI Integration\n\nThe [GitHub](<https://github.com/>) Actions workflow:\n\n```yaml\nname: Semgrep\non: [pull_request]\n\njobs:\n semgrep:\n runs-on: ubuntu-latest\n container:\n image: semgrep/semgrep\n steps:\n - uses: actions/checkout@v4\n\n - name: Run Semgrep\n run: |\n semgrep scan \\\n --config .semgrep/ \\\n --config p/security-audit \\\n --config p/owasp-top-ten \\\n --error \\\n --json \\\n --output results.json \\\n --sarif \\\n --sarif-output results.sarif\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: results.sarif\n if: always()\n\n - name: Comment on PR\n if: failure()\n uses: actions/github-script@v7\n with:\n script: |\n const fs = require('fs');\n const results = JSON.parse(fs.readFileSync('results.json'));\n const findings = results.results || [];\n\n if (findings.length === 0) return;\n\n let body = '## Semgrep Security Findings\\n\\n';\n body += `Found ${findings.length} issue(s):\\n\\n`;\n\n for (const f of findings.slice(0, 10)) {\n body += `- **${f.check_id}** (${f.extra.severity})\\n`;\n body += ` ${f.path}:${f.start.line}\\n`;\n body += ` ${f.extra.message}\\n\\n`;\n }\n\n github.rest.issues.createComment({\n issue_number: context.issue.number,\n owner: context.repo.owner,\n repo: context.repo.repo,\n body\n });\n```\n\n## Results After Six Months\n\nThe numbers tell the story:\n\n```\nBefore Semgrep:\n - Manual security review time: ~45 min/PR\n - PR turnaround: 2.3 days average\n - Security findings per quarter: ~12 (found in review)\n - Missed findings (found in prod): ~4/quarter\n\nAfter Semgrep (+ reduced manual review):\n - Automated scan time: ~90 seconds\n - Manual review (complex PRs only): ~15 min\n - PR turnaround: 4 hours average\n - Security findings per quarter: ~35 (caught in CI)\n - Missed findings (found in prod): 0 last quarter\n\nCustom rules written: 47\n - Security patterns: 23\n - API usage enforcement: 12\n - Architecture guardrails: 8\n - Code quality: 4\n```\n\nThe biggest win is not finding more bugs, it is freeing the security team to do actual threat modeling and architecture review instead of checking whether parameterized queries are used. The mechanical stuff is what machines are good at.\n\n## Frequently asked questions\n\n### What is Semgrep and how does it differ from ESLint or other linters?\n\nSemgrep is a static analysis tool that matches code patterns using Abstract Syntax Tree (AST) analysis rather than text-based regex. Unlike ESLint which is JavaScript-specific, Semgrep supports 30+ languages with the same rule syntax. It also supports cross-function taint analysis for security vulnerability detection, which linters typically cannot do.\n\n### What is taint analysis and when should I use it?\n\nTaint analysis tracks how data flows from untrusted sources (user input, HTTP requests) through your code to dangerous sinks (SQL queries, shell commands, HTML output). Use it when you need to find injection vulnerabilities like SQL injection, XSS, or command injection that depend on data flow rather than individual code patterns.\n\n### Can Semgrep replace human code review?\n\nNo. Semgrep excels at enforcing known patterns consistently, security anti-patterns, API usage rules, framework-specific best practices. But it cannot evaluate business logic correctness, architecture decisions, or code readability. Use it to automate the mechanical checks so human reviewers can focus on the hard stuff.\n\n## Tips\n\n1. **Tip:** Start with Semgrep's ` pattern-not ` before writing complex rules. Most security rules are really just 'find X unless Y is also present.' For example, ` fetch() ` without ` csrf_token ` catches 90% of CSRF issues with a two-pattern rule.\n2. **Tip:** Semgrep's taint mode is where the real power is. Instead of pattern matching individual lines, you define ` sources ` (user input), ` sinks ` (dangerous functions), and ` sanitizers ` (escaping functions). Semgrep traces data flow between them across function boundaries. This catches injection bugs that grep-based tools completely miss.\n3. **Tip:** Run ` semgrep --test ` against your custom rules with both positive and negative test cases. Teams that write rules matching too broadly flood PRs with false positives, killing developer trust in the tool within a week. Test rules like production code.\n\n- semgrep\n- ast\n- code-review\n- security\n- static-analysis\n- taint-analysis\n- custom-rules\n- ci-cd\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Automated Code Review: Semgrep Custom Rules, AST Pattern Matching, and Building Org-Specific Security Scanners","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#title"},{"level":2,"text":"Why Semgrep, Not Regex","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#why-semgrep-not-regex"},{"level":2,"text":"Writing Your First Custom Rule","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#writing-your-first-custom-rule"},{"level":2,"text":"Taint Analysis: Following the Data","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#taint-analysis-following-the-data"},{"level":2,"text":"Building an Org-Specific Rule Set","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#building-an-org-specific-rule-set"},{"level":2,"text":"CI Integration","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#ci-integration"},{"level":2,"text":"Results After Six Months","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#results-after-six-months"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#questions"},{"level":3,"text":"What is Semgrep and how does it differ from ESLint or other linters?","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#question-1"},{"level":3,"text":"What is taint analysis and when should I use it?","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#question-2"},{"level":3,"text":"Can Semgrep replace human code review?","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#article-tips"}],"sections":[{"heading":"Automated Code Review: Semgrep Custom Rules, AST Pattern Matching, and Building Org-Specific Security Scanners","level":1,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules","content":"Developer Tools"},{"heading":"Automated Code Review: Semgrep Custom Rules, AST Pattern Matching, and Building Org-Specific Security Scanners","level":1,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#title","content":"2026-01-155 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/ai-code-review-ast-analysis-semgrep-custom-rules>)"},{"heading":"Why Semgrep, Not Regex","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#why-semgrep-not-regex","content":"The fundamental problem with grep-based security scanning is that code is a tree, not a string. [Semgrep](<https://semgrep.dev/>) addresses this by operating on the Abstract Syntax Tree. Consider finding SQL injection in [Python](<https://www.python.org/>):\n\n```python\n# Grep catches this:\ncursor.execute(\"SELECT * FROM users WHERE id = \" + user_id)\n\n# But misses this (same vulnerability, different formatting):\nquery = \"SELECT * FROM users WHERE id = \" + user_id\ncursor.execute(\n query\n)\n\n# And false-positives on this (safe, parameterized):\ncursor.execute(\"SELECT * FROM users WHERE id = %s\", (user_id,))\n```\n\nSemgrep works on the AST (Abstract Syntax Tree), so it understands code structure, not just text:\n\n```yaml\n# semgrep rule: python-sql-injection.yaml\nrules:\n - id: python-sql-injection\n patterns:\n - pattern: cursor.execute($QUERY, ...)\n - pattern-not: cursor.execute(\"...\", (...))\n - metavariable-pattern:\n metavariable: $QUERY\n patterns:\n - pattern: |\n $X + ...\n - pattern: |\n f\"...\"\n - pattern: |\n \"...\".format(...)\n - pattern: |\n \"...\" % ...\n message: >\n SQL query built with string concatenation or formatting.\n Use parameterized queries instead.\n severity: ERROR\n languages: [python]\n```\n\nThis matches any ` cursor.execute() ` where the query argument is built with concatenation, f-strings, ` .format() `, or ` % ` formatting, but explicitly excludes parameterized queries. It works regardless of whitespace, variable naming, or how many lines the code spans."},{"heading":"Writing Your First Custom Rule","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#writing-your-first-custom-rule","content":"Semgrep rules are YAML files with a surprisingly powerful pattern language. The key building blocks:\n\n```yaml\nrules:\n - id: no-hardcoded-secrets\n patterns:\n # Match: any string assignment that looks like a secret\n - pattern: $KEY = \"...\"\n # Where the variable name suggests it's a secret\n - metavariable-regex:\n metavariable: $KEY\n regex: (?i)(password|secret|api_key|token|private_key)\n # But not when it's empty or a placeholder\n - pattern-not: $KEY = \"\"\n - pattern-not: $KEY = \"CHANGEME\"\n - pattern-not: $KEY = \"placeholder\"\n - pattern-not: $KEY = \"xxx\"\n message: |\n Hardcoded secret detected in variable '$KEY'.\n Use environment variables or a secrets manager instead.\n severity: WARNING\n languages: [python, javascript, typescript, java, go, ruby]\n metadata:\n category: security\n confidence: MEDIUM\n cwe: CWE-798\n```\n\nTest it:\n\n```bash\n# Create test file\ncat > test_secrets.py << 'EOF'\n# ruleid: no-hardcoded-secrets\napi_key = \"sk_live_4242424242424242\"\n\n# ruleid: no-hardcoded-secrets\nDB_PASSWORD = \"super_secret_123\"\n\n# ok: no-hardcoded-secrets\napi_key = \"\"\n\n# ok: no-hardcoded-secrets\npassword = os.environ.get(\"DB_PASSWORD\")\nEOF\n\n# Run the rule\nsemgrep --config python-secrets.yaml test_secrets.py\n```"},{"heading":"Taint Analysis: Following the Data","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#taint-analysis-following-the-data","content":"This is where Semgrep goes beyond pattern matching. Taint mode tracks data flow from sources to sinks:\n\n```yaml\nrules:\n - id: express-xss\n mode: taint\n message: >\n User input flows into HTML response without sanitization.\n This is a Cross-Site Scripting (XSS) vulnerability.\n severity: ERROR\n languages: [javascript, typescript]\n metadata:\n cwe: CWE-79\n owasp: A7:2017\n\n pattern-sources:\n - patterns:\n - pattern: req.query.$PARAM\n - patterns:\n - pattern: req.body.$PARAM\n - patterns:\n - pattern: req.params.$PARAM\n\n pattern-sinks:\n - patterns:\n - pattern: res.send($PAYLOAD)\n - patterns:\n - pattern: res.write($PAYLOAD)\n\n pattern-sanitizers:\n - patterns:\n - pattern: DOMPurify.sanitize(...)\n - patterns:\n - pattern: escape(...)\n - patterns:\n - pattern: encodeURIComponent(...)\n```\n\nThis catches:\n\n```javascript\n// DETECTED: tainted data flows from source to sink\napp.get('/search', (req, res) => {\n const query = req.query.q;\n res.send(`<h1>Results for: ${query}</h1>`); // XSS!\n});\n\n// DETECTED: even through intermediate variables\napp.get('/profile', (req, res) => {\n const userId = req.params.id;\n const name = getUserName(userId);\n const html = `<div>${req.query.displayName}</div>`;\n res.send(html); // XSS via req.query.displayName\n});\n\n// NOT DETECTED: sanitizer breaks the taint chain\napp.get('/safe', (req, res) => {\n const query = DOMPurify.sanitize(req.query.q);\n res.send(`<h1>Results for: ${query}</h1>`); // Safe\n});\n```"},{"heading":"Building an Org-Specific Rule Set","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#building-an-org-specific-rule-set","content":"Beyond security rules, the rule set can expand to organization-specific patterns:\n\n```yaml\n# Rule: Enforce our internal API client usage\nrules:\n - id: use-internal-http-client\n patterns:\n - pattern-either:\n - pattern: fetch(...)\n - pattern: axios.get(...)\n - pattern: axios.post(...)\n - pattern: new XMLHttpRequest()\n - pattern-not-inside: |\n // semgrep-ignore: use-internal-http-client\n ...\n paths:\n include:\n - \"src/**\"\n exclude:\n - \"src/lib/http-client/**\"\n - \"**/*.test.*\"\n message: |\n Use our internal HTTP client (`@myorg/http-client`) instead of\n raw fetch/axios. It handles auth, retries, circuit breaking,\n and distributed tracing automatically.\n severity: WARNING\n languages: [javascript, typescript]\n\n # Rule: Prevent direct database access outside repositories\n - id: no-direct-db-access\n patterns:\n - pattern-either:\n - pattern: prisma.$MODEL.$METHOD(...)\n - pattern: db.query(...)\n - pattern: knex($TABLE)\n - pattern-not-inside: |\n class $REPO {\n ...\n }\n paths:\n include:\n - \"src/**\"\n exclude:\n - \"src/repositories/**\"\n - \"src/migrations/**\"\n message: |\n Direct database access outside the repository layer.\n All database operations should go through repository classes.\n severity: WARNING\n languages: [javascript, typescript]\n```"},{"heading":"CI Integration","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#ci-integration","content":"The [GitHub](<https://github.com/>) Actions workflow:\n\n```yaml\nname: Semgrep\non: [pull_request]\n\njobs:\n semgrep:\n runs-on: ubuntu-latest\n container:\n image: semgrep/semgrep\n steps:\n - uses: actions/checkout@v4\n\n - name: Run Semgrep\n run: |\n semgrep scan \\\n --config .semgrep/ \\\n --config p/security-audit \\\n --config p/owasp-top-ten \\\n --error \\\n --json \\\n --output results.json \\\n --sarif \\\n --sarif-output results.sarif\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: results.sarif\n if: always()\n\n - name: Comment on PR\n if: failure()\n uses: actions/github-script@v7\n with:\n script: |\n const fs = require('fs');\n const results = JSON.parse(fs.readFileSync('results.json'));\n const findings = results.results || [];\n\n if (findings.length === 0) return;\n\n let body = '## Semgrep Security Findings\\n\\n';\n body += `Found ${findings.length} issue(s):\\n\\n`;\n\n for (const f of findings.slice(0, 10)) {\n body += `- **${f.check_id}** (${f.extra.severity})\\n`;\n body += ` ${f.path}:${f.start.line}\\n`;\n body += ` ${f.extra.message}\\n\\n`;\n }\n\n github.rest.issues.createComment({\n issue_number: context.issue.number,\n owner: context.repo.owner,\n repo: context.repo.repo,\n body\n });\n```"},{"heading":"Results After Six Months","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#results-after-six-months","content":"The numbers tell the story:\n\n```\nBefore Semgrep:\n - Manual security review time: ~45 min/PR\n - PR turnaround: 2.3 days average\n - Security findings per quarter: ~12 (found in review)\n - Missed findings (found in prod): ~4/quarter\n\nAfter Semgrep (+ reduced manual review):\n - Automated scan time: ~90 seconds\n - Manual review (complex PRs only): ~15 min\n - PR turnaround: 4 hours average\n - Security findings per quarter: ~35 (caught in CI)\n - Missed findings (found in prod): 0 last quarter\n\nCustom rules written: 47\n - Security patterns: 23\n - API usage enforcement: 12\n - Architecture guardrails: 8\n - Code quality: 4\n```\n\nThe biggest win is not finding more bugs, it is freeing the security team to do actual threat modeling and architecture review instead of checking whether parameterized queries are used. The mechanical stuff is what machines are good at."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#questions","content":""},{"heading":"What is Semgrep and how does it differ from ESLint or other linters?","level":3,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#question-1","content":"Semgrep is a static analysis tool that matches code patterns using Abstract Syntax Tree (AST) analysis rather than text-based regex. Unlike ESLint which is JavaScript-specific, Semgrep supports 30+ languages with the same rule syntax. It also supports cross-function taint analysis for security vulnerability detection, which linters typically cannot do."},{"heading":"What is taint analysis and when should I use it?","level":3,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#question-2","content":"Taint analysis tracks how data flows from untrusted sources (user input, HTTP requests) through your code to dangerous sinks (SQL queries, shell commands, HTML output). Use it when you need to find injection vulnerabilities like SQL injection, XSS, or command injection that depend on data flow rather than individual code patterns."},{"heading":"Can Semgrep replace human code review?","level":3,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#question-3","content":"No. Semgrep excels at enforcing known patterns consistently, security anti-patterns, API usage rules, framework-specific best practices. But it cannot evaluate business logic correctness, architecture decisions, or code readability. Use it to automate the mechanical checks so human reviewers can focus on the hard stuff."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#article-tips","content":"1. **Tip:** Start with Semgrep's ` pattern-not ` before writing complex rules. Most security rules are really just 'find X unless Y is also present.' For example, ` fetch() ` without ` csrf_token ` catches 90% of CSRF issues with a two-pattern rule.\n2. **Tip:** Semgrep's taint mode is where the real power is. Instead of pattern matching individual lines, you define ` sources ` (user input), ` sinks ` (dangerous functions), and ` sanitizers ` (escaping functions). Semgrep traces data flow between them across function boundaries. This catches injection bugs that grep-based tools completely miss.\n3. **Tip:** Run ` semgrep --test ` against your custom rules with both positive and negative test cases. Teams that write rules matching too broadly flood PRs with false positives, killing developer trust in the tool within a week. Test rules like production code.\n\n- semgrep\n- ast\n- code-review\n- security\n- static-analysis\n- taint-analysis\n- custom-rules\n- ci-cd\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/ai-code-review-ast-analysis-semgrep-custom-rules"},{"text":"Semgrep","url":"https://semgrep.dev/"},{"text":"Python","url":"https://www.python.org/"},{"text":"GitHub","url":"https://github.com/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules"},"headline":"Automated Code Review: Semgrep Custom Rules, AST Pattern Matching, and Building Org-Specific Security Scanners","description":"A practical guide to automated code review with Semgrep, writing custom rules with AST pattern matching, taint analysis for tracking untrusted data, building organization-specific security scanners, and integrating everything into CI for automated enforcement.","datePublished":"2026-01-15","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Developer Tools","keywords":"semgrep, ast, code-review, security, static-analysis, taint-analysis, custom-rules, ci-cd","wordCount":965,"timeRequired":"PT5M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/ai-code-review-ast-analysis-semgrep-custom-rules"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Automated Code Review: Semgrep Custom Rules, AST Pattern Matching, and Building Org-Specific Security Scanners","item":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is Semgrep and how does it differ from ESLint or other linters?","acceptedAnswer":{"@type":"Answer","text":"Semgrep is a static analysis tool that matches code patterns using Abstract Syntax Tree (AST) analysis rather than text-based regex. Unlike ESLint which is JavaScript-specific, Semgrep supports 30+ languages with the same rule syntax. It also supports cross-function taint analysis for security vulnerability detection, which linters typically cannot do."}},{"@type":"Question","name":"What is taint analysis and when should I use it?","acceptedAnswer":{"@type":"Answer","text":"Taint analysis tracks how data flows from untrusted sources (user input, HTTP requests) through your code to dangerous sinks (SQL queries, shell commands, HTML output). Use it when you need to find injection vulnerabilities like SQL injection, XSS, or command injection that depend on data flow rather than individual code patterns."}},{"@type":"Question","name":"Can Semgrep replace human code review?","acceptedAnswer":{"@type":"Answer","text":"No. Semgrep excels at enforcing known patterns consistently, security anti-patterns, API usage rules, framework-specific best practices. But it cannot evaluate business logic correctness, architecture decisions, or code readability. Use it to automate the mechanical checks so human reviewers can focus on the hard stuff."}}]}]}],"contentHash":"9d8e0f06acaf5312cfd6ce96b6b3a3d2c3fe18394a5e9201678b5f2a217c24e3","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/ai-code-review-ast-analysis-semgrep-custom-rules","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":56565,"contentBytes":10203}}
