Custom Semgrep rules in .semgrep/<rule>.yaml are picked up by semgrep --config .semgrep/. Custom rules consistently produce the highest signal-to-noise findings because they encode your house conventions and internal frameworks.
Every route handler in routes/ must have @require_auth(scope=...) or @public_endpoint.
# .semgrep/internal-auth-required.yaml
rules:
- id: internal-auth-required
message: Route handler is missing @require_auth or @public_endpoint
severity: ERROR
languages: [python]
paths:
include: ["routes/**"]
patterns:
- pattern: |
@app.$METHOD(...)
def $FN(...):
...
- pattern-not-inside: |
@require_auth(...)
@app.$METHOD(...)
def $FN(...):
...
- pattern-not-inside: |
@public_endpoint
@app.$METHOD(...)
def $FN(...):
...Any logger call that includes a known PII field name must wrap it in redact().
# .semgrep/pii-redaction-required.yaml
rules:
- id: pii-redaction-required
message: PII field logged without redact()
severity: ERROR
languages: [python]
pattern-either:
- pattern: logger.$LEVEL(..., email=$EMAIL, ...)
- pattern: logger.$LEVEL(..., ssn=$SSN, ...)
- pattern: logger.$LEVEL(..., phone=$PHONE, ...)
- pattern: logger.$LEVEL(..., card_number=$CC, ...)
pattern-not: logger.$LEVEL(..., $FIELD=redact($VAL), ...)The internal HTTP client acme_http.get() must always pass timeout= and verify=True.
# .semgrep/internal-http-client-misuse.yaml
rules:
- id: internal-http-missing-timeout
message: acme_http.$METHOD called without timeout
severity: ERROR
languages: [python]
patterns:
- pattern: acme_http.$METHOD(...)
- pattern-not: acme_http.$METHOD(..., timeout=$T, ...)
- id: internal-http-verify-disabled
message: acme_http.$METHOD called with verify=False
severity: ERROR
languages: [python]
pattern: acme_http.$METHOD(..., verify=False, ...)Block import requests in modules that should use the internal client; block import pickle in services that accept external input.
# .semgrep/forbidden-imports.yaml
rules:
- id: prefer-internal-http-client
message: Use acme_http instead of requests
severity: ERROR
languages: [python]
paths:
include: ["services/**"]
pattern-either:
- pattern: import requests
- pattern: from requests import ...
- id: pickle-in-public-service
message: pickle is forbidden in services that accept external input
severity: ERROR
languages: [python]
paths:
include: ["services/public/**"]
pattern-either:
- pattern: import pickle
- pattern: from pickle import ...Every DB query in a multi-tenant service must include a tenant_id filter.
# .semgrep/tenant-scope-required.yaml
rules:
- id: tenant-scope-required
message: Query missing tenant_id filter in multi-tenant service
severity: ERROR
languages: [python]
paths:
include: ["services/multi_tenant/**"]
patterns:
- pattern-either:
- pattern: $MODEL.query.filter_by(...)
- pattern: db.session.query($MODEL).filter(...)
- pattern-not-inside: |
$MODEL.query.filter_by(..., tenant_id=$T, ...)
- pattern-not-inside: |
db.session.query($MODEL).filter(..., $MODEL.tenant_id == $T, ...)- Start with one canonical violation in your repo; build the rule against it; expand
pattern-eitheras you find variants. - Always include
paths.includeto scope the rule - a global custom rule generates noise. - Use
severity: ERRORfor rules you want CI to block on;WARNINGfor advisories. - Add a
metadata.fix:block when the auto-fix is unambiguous. - Group related rules into one file (e.g.,
internal-http-client.yamlcovers both timeout and verify). - Run
semgrep --config .semgrep/ --testagainst fixture files intests/semgrep-fixtures/to keep rules honest. - Review rules quarterly - frameworks evolve, paths change, conventions shift.