| name | semgrep-review |
| description | Analyze semgrep scan findings, triage real vulnerabilities vs false positives, fix real issues using framework-native escaping, and suppress false positives with properly formatted nosemgrep comments. |
Semgrep Review Skill
You are an expert at analyzing semgrep static analysis findings. Your job is to
triage findings into real issues vs false positives, fix real issues, and
suppress false positives with well-documented nosemgrep comments.
Workflow
Step 1: Run Semgrep
If the user hasn't provided semgrep output, run the scan.
First, check if the project has local semgrep rules (a .semgrep/ directory or
.semgrep.yaml file in the project root). If local rules exist, include them
alongside the auto config:
semgrep scan --config auto --config .semgrep <target-directory>
semgrep scan --config auto <target-directory>
Using --config auto --config .semgrep ensures local custom rules are applied
on top of the semgrep registry defaults. Omitting the local config when it
exists can cause semgrep to fail.
Step 2: Triage Each Finding
For every finding, determine whether it is a real issue or a false
positive by reading the flagged code and tracing the data source.
Questions to ask for each finding:
- Where does the flagged value originate? (user input, database, config, test
fixture)
- Is the value sanitized or escaped before use? (framework auto-escaping,
manual escaping)
- What is the threat model? (public-facing, admin-only, test-only)
Common false positive categories:
| Category | Example | Why It's Safe |
|---|
| Test fixtures | Private keys, hashes in test files | Intentionally committed, no real system access |
| Framework-escaped values | Variables inside Hono html template literals, React JSX | Framework auto-escapes interpolated values |
| Admin-only internal tools | Values from admin DB displayed in admin panel | Not user-controlled, but still fix for defense-in-depth |
Step 3: Fix Real Issues
Prefer framework-native escaping over custom helpers. Do not write a custom
escapeHtml() function if the framework provides auto-escaping.
Common fix patterns by framework:
Hono (html tagged template literals)
Replace manual string interpolation with html template literals:
const items = data.map((d) => `<span>${d.name}</span>`).join('');
return raw(items);
const items = data.map((d) => html`<span>${d.name}</span>`);
return items;
When refactoring from raw() + string templates to html templates:
- Replace backtick strings with
html tagged template literals
- Remove
.join('') calls (arrays of HtmlEscapedString render correctly)
- Remove
raw() wrappers (no longer needed when content is already
HtmlEscapedString)
- Watch for extra
) characters left behind when removing raw( wrapper
React / JSX
JSX auto-escapes by default. Flag only dangerouslySetInnerHTML usage:
<div dangerouslySetInnerHTML={{ __html: userInput }} />
<div>{userInput}</div>
Plain HTML / Server-side rendering
Use the framework's escape utility or a well-known library. As a last resort,
write a standard escape function:
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Step 4: Suppress False Positives with nosemgrep
Syntax Rules
The nosemgrep comment must be:
- On the same line as the finding, OR
- Alone on the line immediately above the finding
Use the short rule ID (e.g., detected-private-key), not the full dotted
path (e.g., generic.secrets.security.detected-private-key.detected-private-key).
const key = `-----BEGIN PRIVATE KEY-----...`;
const key = `-----BEGIN PRIVATE KEY-----...`;
const key = `-----BEGIN PRIVATE KEY-----...`;
Documenting the Rationale
Always add a comment explaining why the suppression is safe:
const TEST_KEY = `-----BEGIN PRIVATE KEY-----...`;
For bcrypt hashes in tests:
expect(needsRehash('$2a$10$...')).toBe(true);
Suppressing Inside Template Literals
JavaScript // and /* */ comments don't work inside template literal text.
Use these techniques instead:
Inside ${} expressions (these are JS expression context):
html`<div>
${/* nosemgrep: unknown-value-with-script-tag */ renderItems(data)}
</div>`;
html`<input value=${
getValue() // nosemgrep: unknown-value-with-script-tag
} />`;
For object properties inside ${}:
html`${textarea({
name: 'field',
value: data.join('\n'), // nosemgrep: unknown-value-with-script-tag
rows: 2,
})}`;
When no JS expression context is available, extract the value to a variable
above the template:
const safeValue = computeValue();
html`<div>${safeValue}</div>`;
Note: The nosemgrep comment must be on the specific line semgrep flags,
not just anywhere nearby. Run semgrep after adding comments to verify
suppression works.
Step 5: Verify
After all fixes and suppressions:
- Re-run semgrep (with
--config auto --config .semgrep if local rules exist) to confirm 0 findings
- Run type checking / build to confirm no regressions
- Run tests if the changes affect runtime behavior
Output Format
Present findings as a summary table:
| # | File | Rule | Verdict | Action |
|---|
| 1 | path/to/file.ts:42 | rule-id | Real issue | Fix: use html template |
| 2 | path/to/test.ts:10 | detected-private-key | False positive | Suppress: test fixture |
Then proceed with fixes and suppressions.