Skip to content

feat: implement enforced Semgrep scanning with clean baseline (#2984)#3406

Merged
kasya merged 19 commits intoOWASP:mainfrom
SpruhaCK:add-semgrep-checks
Jan 23, 2026
Merged

feat: implement enforced Semgrep scanning with clean baseline (#2984)#3406
kasya merged 19 commits intoOWASP:mainfrom
SpruhaCK:add-semgrep-checks

Conversation

@SpruhaCK
Copy link
Contributor

@SpruhaCK SpruhaCK commented Jan 18, 2026

Proposed change

Resolves #2984

This PR centralizes the Semgrep security pipeline into the Makefile to ensure identical security enforcement for both local development and CI/CD.

Key Changes

1. Centralized Architecture (Makefile)

  • Single Source of Truth: Centralized the scan logic in the Makefile (security-scan and security-scan-ci).
  • OSS-First Strategy: I opted for explicit rule definitions in the Makefile instead of a semgrep.yml to allow the project to use high-quality rule packs (like p/owasp-top-ten) without forcing contributors to create a Semgrep SaaS account.
  • Consistency: Both Pre-commit and GitHub Actions now call these Makefile targets, ensuring developers and the CI see the exact same results.

2. CI/CD Integration

  • Blocking Pipeline: Integrated Semgrep as a required job in run-ci-cd.yaml. It now correctly blocks merges if high-severity issues are detected.
  • Optimized Logs: CI output is sent to stdout for immediate visibility in GitHub logs, while local scans still generate a findings.txt for developer convenience. Prevents losing findings in the terminal scrollback.

3. Resolved Security Findings

  • XSS Mitigation: Fixed injection vulnerabilities in layout.tsx and StructuredDataScript.tsx by implementing DOMPurify sanitization and adding regex validation for the organizationKey parameter.
  • Handled False Positives: Cleaned the baseline by using inline # nosemgrep comments for safe patterns (like internal loggers and Nginx headers), providing documented justifications for each.

💻 Local Verification Command

  • Standard Check:
    pre-commit run --all-files

  • Detailed Report:
    make security-scan

Checklist

  • Required: I followed the contributing workflow.
  • Required: I varified make security-scan successfully scanned 1720 files in 31 seconds with 0 Findings.
  • Required: I ran make check-test locally and all checks pass except the frontend tests whict are there in main branch
  • I used AI for drafting this technical documentation.

@github-actions github-actions bot added the ci label Jan 18, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Warning

Rate limit exceeded

@arkid15r has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

This PR integrates Semgrep security scanning into the CI/CD pipeline, adds defensive HTML sanitization to frontend components using DOMPurify, hardens Django admin rendering with format_html, and annotates existing code with nosemgrep directives to suppress specific security lint warnings.

Changes

Cohort / File(s) Summary
CI/CD & Security Scanning Infrastructure
.github/workflows/run-ci-cd.yaml, Makefile, .github/dependabot.yml, docker/semgrep/Dockerfile
Adds run-security-scan workflow job that executes Semgrep via Docker, archives results, and now gates build-staging-images and build-production-images jobs. Makefile includes security-scan target. Dependabot configured for /docker/semgrep Docker updates.
Backend Code — Static Analysis Suppressions
backend/apps/api/decorators/cache.py, backend/apps/api/internal/mutations/api_key.py, backend/apps/slack/views.py
Adds inline nosemgrep/noqa directives and explanatory comments to existing code paths; no functional changes to logic or control flow.
Backend Admin — Security Hardening
backend/apps/github/admin/issue.py, backend/apps/github/admin/pull_request.py, backend/apps/github/admin/repository.py, backend/apps/owasp/admin/mixins.py, backend/apps/owasp/admin/widgets.py
Replaces unsafe mark_safe HTML generation with Django's format_html for safer parameterized HTML escaping across admin URL rendering.
Frontend — Security Improvements
frontend/package.json, frontend/src/app/organizations/[organizationKey]/layout.tsx, frontend/src/components/StructuredDataScript.tsx
Adds isomorphic-dompurify dependency. Introduces DOMPurify sanitization for structured JSON-LD injection. Adds organizationKey validation guard (rejects keys with invalid characters). Computes sanitized jsonLdString before Script tag injection.
Proxy & Web Server — Static Analysis Suppressions
frontend/nginx.conf, proxy/production.conf, proxy/staging.conf, proxy/redirects.conf
Adds explanatory comments and inline nosemgrep tags around Host header forwarding and redirect directives; no functional routing or configuration changes.
Configuration & Build Files
cspell/Dockerfile, docker/cspell/Dockerfile, cspell/custom-dict.txt, .gitignore
Adds nosemgrep directives to Dockerfiles. Expands cspell dictionary with "nosemgrep" and "semgrep". Updates .gitignore for frontend build artifacts and security reports.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

backend, docker, frontend, security

Suggested reviewers

  • kasya
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing enforced Semgrep scanning with a clean baseline, which is the primary objective of this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the Semgrep integration, key changes, security fixes, and verification commands.
Linked Issues check ✅ Passed The PR addresses all objectives from issue #2984: adds Semgrep as a static analysis scanner [#2984], integrates into local workflows and CI/CD [#2984], uses OSS-friendly approach without requiring SaaS accounts [#2984], and resolves XSS vulnerabilities [#2984].
Out of Scope Changes check ✅ Passed All changes are within scope: Makefile security targets, CI/CD integration, XSS fixes, baseline cleanup with nosemgrep comments, and dependency additions (isomorphic-dompurify, Dependabot Docker config) directly support Semgrep implementation.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.github/workflows/semgrep.yml:
- Around line 24-30: Update the "Run Semgrep" step so it no longer pulls
:latest; either pin the Docker image to the specific version used by pre-commit
(change returntocorp/semgrep to returntocorp/semgrep:v1.104.0) or replace the
step with the official action (uses: semgrep/semgrep-action@v1) which provides
caching and SARIF support; modify the step name "Run Semgrep" and its run/uses
invocation accordingly and ensure the same config entries (p/owasp-top-ten,
p/python, p/javascript) are preserved.

In @.pre-commit-config.yaml:
- Around line 90-95: Update the semgrep hook rev from v1.104.0 to v1.148.0 in
the pre-commit configuration: locate the semgrep repo block (repo:
https://github.com/semgrep/semgrep) and change the rev field to v1.148.0 so
local pre-commit scans use the latest rulesets referenced by the args
(--config=p/owasp-top-ten, --config=p/python, --config=p/javascript, --error).
🧹 Nitpick comments (1)
.github/workflows/semgrep.yml (1)

16-19: Add a timeout to prevent hung jobs.

Consider adding timeout-minutes to prevent indefinitely running jobs if Semgrep hangs on complex files.

Suggested change
 jobs:
   semgrep:
     name: Run Semgrep Security Scan
     runs-on: ubuntu-latest
+    timeout-minutes: 15
     steps:

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 18, 2026
@SpruhaCK
Copy link
Contributor Author

All changes addressed. Switched to official action, pinned to SHA hashes for security, and added the requested timeout.

Copy link
Collaborator

@arkid15r arkid15r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good start, you need to shape it properly now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it to be part of the main ci/cd pipeline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. I'll integrate the Semgrep job directly into the main CI/CD workflow i.e. run-ci-cd.yaml as a required dependency.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use consistent syntax

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also use centralized configuration file for both pre-commit and ci/cd workflow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, we can create a centralized .semgrep.yml file in the root directory to act as the single source of truth. I’ll then refactor the .pre-commit-config.yaml and the CI/CD workflow to point to it so the rules stay consistent everywhere. I'll get started on this refactor and push the changes for your review!

.semgrepignore Outdated
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be fixed -- not ignored. What's the exact issue with this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue Semgrep flagged in layout.tsx was a potential Client-Side Injection vulnerability. Because the organizationKey is a dynamic route parameter, Semgrep identified it as untrusted user input that was being used in a context (like a metadata field or a direct UI element) where it could lead to XSS if not handled carefully. I used .semgrepignore to keep the initial CI setup clean, but I'm now looking at implementing a proper fix either by ensuring the input is strictly validated or by using a safe React rendering pattern to neutralize the risk.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking of using a strict regex validation for organizationKey prop or is there a centralized utility in the Nest codebase you'd prefer I use for sanitizing these route parameters?

@arkid15r arkid15r marked this pull request as draft January 19, 2026 01:51
@arkid15r arkid15r requested a review from noland-crane January 19, 2026 19:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.github/workflows/run-ci-cd.yaml:
- Around line 63-86: The semgrep job has duplicate step names, redundant
targets, an unpinned semgrep install, and an inconsistent checkout SHA; fix by
(1) keeping only one security scan step (prefer running the CI-focused make
target security-scan-ci and remove the security-scan step) and give that step a
unique name like "Run Semgrep CI Scan", (2) pin the Semgrep install by changing
the pip install call to install the same version used by pre-commit (e.g., pip
install semgrep==1.148.0), and (3) standardize the actions/checkout reference to
the same SHA used in other jobs (replace `@b4ffde65f463`... with
`@8e8c483db84b`...). Update the semgrep job block and step names accordingly.

In `@Makefile`:
- Around line 99-100: Add the missing target name "security-scan-ci" to the
.PHONY declaration so the Makefile's phony targets list includes
security-scan-ci and avoids conflicts with any file of that name; update the
existing .PHONY line (the declaration containing build clean check pre-commit
prune run scan-images security-scan test update) to also include
security-scan-ci.
🧹 Nitpick comments (6)
proxy/production.conf (1)

82-83: Minor: Comment text is slightly inaccurate for redirect context.

The comment mentions "reverse proxy configuration" and "Host header forwarding," but this is an HTTP→HTTPS redirect, not a proxy pass. The suppression itself is valid since server_name nest.owasp.org; (line 74) ensures only matching hosts reach this block.

Consider using the literal hostname for defense-in-depth:

Suggested improvement
-        # Reason: Standard reverse proxy configuration; Host header forwarding is required for routing.
-        return 301 https://$host$request_uri; # nosemgrep: generic.nginx.security.request-host-used.request-host-used
+        # Reason: Redirect HTTP to HTTPS; server_name ensures only valid hosts reach this block.
+        return 301 https://nest.owasp.org$request_uri;

Using the literal hostname eliminates the need for the nosemgrep suppression entirely and removes any theoretical Host header manipulation concern.

frontend/src/components/StructuredDataScript.tsx (1)

10-16: DOMPurify may corrupt JSON-LD data; consider JSON-specific escaping instead.

While the security intent is correct and the static analysis warnings are false positives (sanitization is applied), DOMPurify is designed to sanitize HTML, not JSON. It may strip or modify valid JSON content that resembles HTML tags (e.g., a description containing <strong> or similar).

For JSON-LD in <script type="application/ld+json"> tags, JSON.stringify() already safely escapes content. The primary injection concern is </script> sequences, which can be handled more precisely:

♻️ Consider targeted JSON escaping instead
-import DOMPurify from 'isomorphic-dompurify'
 import { ProfilePageStructuredData } from 'types/profilePageStructuredData'

 interface StructuredDataScriptProps {
   data: ProfilePageStructuredData
 }

 const StructuredDataScript: React.FC<StructuredDataScriptProps> = ({ data }) => {
-  const cleanData = DOMPurify.sanitize(JSON.stringify(data, null, 2))
+  // Escape </script> sequences to prevent breaking out of script context
+  const cleanData = JSON.stringify(data, null, 2).replace(/<\/script/gi, '<\\/script')
   return (

Based on learnings, DOMPurify is the established pattern in this project for dangerouslySetInnerHTML. If the team prefers consistency over precision, the current approach is acceptable—just be aware it may alter data containing HTML-like strings.

frontend/src/app/organizations/[organizationKey]/layout.tsx (1)

112-114: Same DOMPurify-on-JSON consideration applies here.

The static analysis warnings (lines 128-129) are false positives since sanitization is applied. However, the same concern from StructuredDataScript.tsx applies: DOMPurify may alter valid JSON containing HTML-like strings. Consider a consistent approach across both files—either DOMPurify for uniformity with project patterns, or targeted </script> escaping for precision.

docker/cspell/Dockerfile (1)

19-24: Clarify the USER directive placement.

The USER node directive on line 24 appears after ENTRYPOINT, which means the entrypoint still runs as root (the default). If the intent is to run cspell as the node user, move the USER directive before ENTRYPOINT. If root is truly required (as the comment suggests), consider removing the USER node line to avoid confusion.

Option A: If cspell should run as node user
 WORKDIR /nest
-# Reason: This is a dev-tool linter, not a production service. Root access is required for IO operations.
-# nosemgrep: dockerfile.security.missing-user-entrypoint.missing-user-entrypoint
-ENTRYPOINT ["/opt/node/node_modules/.bin/cspell"]
 
 USER node
+
+ENTRYPOINT ["/opt/node/node_modules/.bin/cspell"]
Option B: If root is required, remove misleading USER directive
 WORKDIR /nest
 # Reason: This is a dev-tool linter, not a production service. Root access is required for IO operations.
 # nosemgrep: dockerfile.security.missing-user-entrypoint.missing-user-entrypoint
 ENTRYPOINT ["/opt/node/node_modules/.bin/cspell"]
-
-USER node
.pre-commit-config.yaml (1)

91-109: Configuration drift: missing timeout settings.

The Makefile's SEMGREP_CONFIGS includes --timeout 10 and --timeout-threshold 3, but this pre-commit hook configuration lacks these settings. This could cause timeouts during pre-commit runs that wouldn't occur with make security-scan.

Additionally, per the past discussion, consider extracting these configs into a centralized .semgrep.yml file to maintain consistency.

Add timeout settings for consistency
           - --config
           - p/secrets
           - --error
+          - --timeout
+          - '10'
+          - --timeout-threshold
+          - '3'
Makefile (1)

70-77: Success message may be misleading when findings exist.

When semgrep finds issues and --error is used, the command exits non-zero, so the "Scan Complete" echo won't execute. However, users may expect the findings file to always be created. Consider adjusting the flow to always save findings regardless of exit code:

Proposed alternative to always capture findings
 security-scan:
 	`@echo` "🛡️  Running Centralized Security Scan..."
-	semgrep $(SEMGREP_CONFIGS) --error --output findings.txt .
-	`@echo` "✅ Scan Complete. Detailed findings saved to findings.txt"
+	semgrep $(SEMGREP_CONFIGS) --output findings.txt . || true
+	`@echo` "✅ Scan Complete. Findings saved to findings.txt"
+	`@semgrep` $(SEMGREP_CONFIGS) --error . > /dev/null 2>&1

Alternatively, keep the current behavior if you want local scans to fail fast on findings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Makefile`:
- Around line 8-16: The SEMGREP_CONFIGS variable in the Makefile ends with a
trailing backslash which causes the following build: target to be folded into
the variable; remove the final backslash after "--timeout-threshold 3" so
SEMGREP_CONFIGS is a complete variable value (leave the preceding lines and
backslashes intact) and ensure the subsequent build: target starts on its own
line outside the variable definition.
♻️ Duplicate comments (1)
.github/workflows/run-ci-cd.yaml (1)

70-71: Align checkout action SHA with other jobs.
Line 71 uses a different actions/checkout pin than the rest of the workflow; please standardize on the same SHA for consistency and supply-chain tracking.

🛠️ Proposed fix
-        uses: actions/checkout@b4ffde65f463366882ddc3bfe93e684e7eeb5e40
+        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 21, 2026
@SpruhaCK SpruhaCK requested a review from arkid15r January 21, 2026 15:59
@arkid15r arkid15r marked this pull request as ready for review January 21, 2026 20:13
Copy link
Collaborator

@noland-crane noland-crane left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add artifacts for the tool out put. In the .pre-commit-config.yaml file under the semgrep job at a step for save the file to an artifact. Here is an example:

- name: Archive Semgrep Project Report
        # 'always' ensures the report is saved even if Semgrep finds vulnerabilities
        if: always() 
        uses: actions/upload-artifact@v4
        with:
          # This is the name of the ZIP file you download from GitHub
          name: semgrep-results-run-${{ github.run_number }}
          # This MUST match the filename created in the Makefile above
          path: semgrep-security-report.txt
          # Optional: Keep the report for only 14 days to save space
          retention-days: 14

here is the documentation for GitHub artifacts

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 22, 2026
@SpruhaCK SpruhaCK requested a review from noland-crane January 22, 2026 16:23
Improve security scan implementation.

Co-authored-by: kart-u <kart-u@users.noreply.github.com>
Co-authored-by: Noland Crane <noland-crane@users.noreply.github.com>
@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 68.75000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.59%. Comparing base (7484604) to head (5da4042).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
backend/apps/owasp/admin/mixins.py 20.00% 4 Missing ⚠️
frontend/src/components/StructuredDataScript.tsx 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3406      +/-   ##
==========================================
- Coverage   85.60%   85.59%   -0.02%     
==========================================
  Files         461      461              
  Lines       14222    14224       +2     
  Branches     1894     1894              
==========================================
  Hits        12175    12175              
- Misses       1678     1680       +2     
  Partials      369      369              
Flag Coverage Δ
backend 84.47% <73.33%> (-0.01%) ⬇️
frontend 88.72% <0.00%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
backend/apps/api/decorators/cache.py 96.15% <100.00%> (ø)
backend/apps/api/internal/mutations/api_key.py 88.23% <100.00%> (ø)
backend/apps/github/admin/issue.py 100.00% <100.00%> (ø)
backend/apps/github/admin/pull_request.py 100.00% <100.00%> (ø)
backend/apps/github/admin/repository.py 100.00% <100.00%> (ø)
backend/apps/owasp/admin/widgets.py 100.00% <100.00%> (ø)
backend/apps/slack/views.py 100.00% <100.00%> (ø)
frontend/src/components/StructuredDataScript.tsx 0.00% <0.00%> (ø)
backend/apps/owasp/admin/mixins.py 53.42% <20.00%> (-0.75%) ⬇️

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7484604...5da4042. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.github/workflows/run-ci-cd.yaml:
- Around line 418-424: The Archive Semgrep Project Report workflow step
currently pins actions/upload-artifact to a different SHA
(actions/upload-artifact@65c86da0...) than the other usages; update the pinned
reference in the "Archive Semgrep Project Report" step (the step named "Archive
Semgrep Project Report" that uses actions/upload-artifact) to the same SHA used
elsewhere (actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f) so
all upload-artifact references are consistent for supply-chain integrity.
🧹 Nitpick comments (1)
Makefile (1)

61-83: Good implementation of the security scan target.

The target correctly mounts the repo, applies multiple rule packs, and fails on findings with --error. Minor observation: if docker/semgrep/Dockerfile doesn't exist or lacks the expected FROM semgrep/semgrep: line, line 66 will produce an empty string, causing a cryptic Docker error.

Consider adding a guard:

Optional hardening
 security-scan:
 	`@echo` "Running Security Scan..."
+	`@test` -f docker/semgrep/Dockerfile || { echo "Error: docker/semgrep/Dockerfile not found"; exit 1; }
 	`@docker` run --rm \
 		-v "$(PWD):/src" \
 		-w /src \

@sonarqubecloud
Copy link

Copy link
Collaborator

@arkid15r arkid15r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@arkid15r arkid15r added this pull request to the merge queue Jan 23, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 23, 2026
@kasya kasya added this pull request to the merge queue Jan 23, 2026
Merged via the queue into OWASP:main with commit e872f1e Jan 23, 2026
33 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Semgrep to local and CI/CD checks

4 participants

Comments