Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8d4456c
feat: implement enforced Semgrep scanning with clean baseline (#2984)
SpruhaCK Jan 18, 2026
8cc47c8
chore: address CodeRabbit review (pin versions, optimize workflow, an…
SpruhaCK Jan 18, 2026
7dbe9cf
fix: resolve SonarCloud security hotspot by hardening permissions and…
SpruhaCK Jan 18, 2026
0a2d82b
fix: pin github actions to SHA hashes to resolve sonarcloud hotspot
SpruhaCK Jan 18, 2026
7a4b09a
Refactor semgrep and update CI/CD workflows
SpruhaCK Jan 19, 2026
a3f9287
Refactor semgrep and update CI workflows
SpruhaCK Jan 19, 2026
180901e
Merge branch 'main' of https://github.com/owasp/nest into semgrep-ref…
SpruhaCK Jan 19, 2026
c22ec83
backup: save all refactor work and formatting
SpruhaCK Jan 21, 2026
9ba1a8c
Merge branch 'main' into add-semgrep-checks
SpruhaCK Jan 21, 2026
65ab648
Merge branch 'semgrep-refactor' into add-semgrep-check
SpruhaCK Jan 21, 2026
4109040
chore: final cleanup
SpruhaCK Jan 21, 2026
56b3c69
chore: address CodeRabbit review (pin versions and fix redundant steps)
SpruhaCK Jan 21, 2026
ea7a930
chore: address CodeRabbit review
SpruhaCK Jan 21, 2026
a2bb8e3
Updated cspell/custom-dict.txt
SpruhaCK Jan 21, 2026
7b49f9a
Merge branch 'main' into add-semgrep-checks
SpruhaCK Jan 22, 2026
ba59528
refactor: add artifact archiving
SpruhaCK Jan 22, 2026
441b7f9
Consolidate ci/cd and local command run.
arkid15r Jan 23, 2026
491dfe3
Merge branch 'main' into add-semgrep-checks
arkid15r Jan 23, 2026
5da4042
Update code
arkid15r Jan 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ updates:
schedule:
interval: daily

- package-ecosystem: docker
directory: /docker/semgrep
schedule:
interval: daily

- package-ecosystem: github-actions
directory: /
schedule:
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/run-ci-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,30 @@ jobs:
docker run --env-file frontend/.env.example owasp/nest:test-frontend-a11y-latest pnpm run test:a11y
timeout-minutes: 5

run-security-scan:
name: Run security scan
needs:
- scan-code
- scan-ci-dependencies
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8

- name: Run Security CI Scan
run: make security-scan

- name: Upload security scan report
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: semgrep-results-run-${{ github.run_number }}
path: semgrep-security-report.txt
retention-days: 14
timeout-minutes: 15

set-release-version:
name: Set release version
outputs:
Expand Down Expand Up @@ -450,6 +474,7 @@ jobs:
- run-frontend-e2e-tests
- run-frontend-unit-tests
- set-release-version
- run-security-scan
permissions:
contents: read
runs-on: ubuntu-latest
Expand Down Expand Up @@ -820,6 +845,7 @@ jobs:
- run-frontend-e2e-tests
- run-frontend-unit-tests
- set-release-version
- run-security-scan
permissions:
contents: read
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ design/
frontend/blob-report/
frontend/coverage
frontend/dist
frontend/.next
frontend/out
frontend/npm-debug.log*
frontend/playwright-report/
frontend/playwright/.cache/
Expand All @@ -50,3 +52,4 @@ venv/

# Snyk Security Extension - AI Rules (auto-generated)
.cursor/rules/snyk_rules.mdc
semgrep-security-report.txt
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ scan-images: \
scan-backend-image \
scan-frontend-image

security-scan:
@echo "Running Security Scan..."
@docker run --rm \
-v "$(PWD):/src" \
-w /src \
$$(grep -E '^FROM semgrep/semgrep:' docker/semgrep/Dockerfile | sed 's/^FROM //') \
semgrep \
--config p/ci \
--config p/javascript \
--config p/nginx \
--config p/owasp-top-ten \
--config p/python \
--config p/secrets \
--config p/security-audit \
--config p/sql-injection \
--config p/typescript \
--error \
--skip-unknown-extensions \
--timeout 10 \
--timeout-threshold 3 \
--text \
--text-output=semgrep-security-report.txt \
.

test: \
test-nest-app

Expand All @@ -78,3 +102,5 @@ update-nest-app-dependencies: \

update-pre-commit:
@pre-commit autoupdate

.PHONY: build clean check pre-commit prune run scan-images security-scan test update
3 changes: 2 additions & 1 deletion backend/apps/api/decorators/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def generate_key(
prefix: str,
):
"""Generate a cache key for a request."""
return f"{prefix}:{request.get_full_path()}"
# Reason: This generates an internal cache key, not a user-facing HTML response.
return f"{prefix}:{request.get_full_path()}" # nosemgrep: python.flask.security.audit.directly-returned-format-string.directly-returned-format-string # noqa: E501


def cache_response(
Expand Down
5 changes: 4 additions & 1 deletion backend/apps/api/internal/mutations/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ def create_api_key(self, info: Info, name: str, expires_at: datetime) -> CreateA
message="API key created successfully.",
)
except IntegrityError as err:
logger.warning("Error creating API key: %s", err)
# Reason: Logging the error message string only, not the sensitive API key credential itself. # noqa: E501
logger.warning( # nosemgrep: python.lang.security.audit.logging.logger-credential-leak.python-logger-credential-disclosure # noqa: E501
"Error creating API key: %s", err
)
return CreateApiKeyResult(
ok=False,
code="ERROR",
Expand Down
4 changes: 2 additions & 2 deletions backend/apps/github/admin/issue.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""GitHub app Issue model admin."""

from django.contrib import admin
from django.utils.safestring import mark_safe
from django.utils.html import format_html

from apps.github.models.issue import Issue

Expand Down Expand Up @@ -38,7 +38,7 @@ def custom_field_github_url(self, obj) -> str:
str: A safe HTML link to the issue on GitHub.

"""
return mark_safe(f"<a href='{obj.url}' target='_blank'>↗️</a>") # noqa: S308
return format_html("<a href='{}' target='_blank'>↗️</a>", obj.url)

custom_field_github_url.short_description = "GitHub 🔗"

Expand Down
4 changes: 2 additions & 2 deletions backend/apps/github/admin/pull_request.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""GitHub app PullRequest model admin."""

from django.contrib import admin
from django.utils.safestring import mark_safe
from django.utils.html import format_html

from apps.github.models.pull_request import PullRequest

Expand Down Expand Up @@ -44,7 +44,7 @@ def custom_field_github_url(self, obj: PullRequest) -> str:
str: A safe HTML link to the pull request on GitHub.

"""
return mark_safe(f"<a href='{obj.url}' target='_blank'>↗️</a>") # noqa: S308
return format_html("<a href='{}' target='_blank'>↗️</a>", obj.url)

custom_field_github_url.short_description = "GitHub 🔗"

Expand Down
8 changes: 5 additions & 3 deletions backend/apps/github/admin/repository.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""GitHub app Repository model admin."""

from django.contrib import admin
from django.utils.safestring import mark_safe
from django.utils.html import format_html

from apps.github.models.repository import Repository

Expand Down Expand Up @@ -46,8 +46,10 @@ def custom_field_github_url(self, obj) -> str:
str: A safe HTML link to the repository on GitHub.

"""
return mark_safe( # noqa: S308
f"<a href='https://github.com/{obj.owner.login}/{obj.name}' target='_blank'>↗️</a>"
return format_html(
"<a href='https://github.com/{}/{}' target='_blank'>↗️</a>",
obj.owner.login,
obj.name,
)

def custom_field_title(self, obj: Repository) -> str:
Expand Down
21 changes: 10 additions & 11 deletions backend/apps/owasp/admin/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.contrib.contenttypes.admin import GenericTabularInline
from django.contrib.contenttypes.models import ContentType
from django.utils.html import escape
from django.utils.html import format_html
from django.utils.safestring import mark_safe

from apps.owasp.admin.widgets import ChannelIdWidget
Expand Down Expand Up @@ -106,20 +106,18 @@ def custom_field_github_urls(self, obj):
return ""
return self._format_github_link(obj.owasp_repository)

return mark_safe( # noqa: S308
" ".join(
[self._format_github_link(repository) for repository in obj.repositories.all()]
)
links = [self._format_github_link(repository) for repository in obj.repositories.all()]
# Use mark_safe since links are already safely formatted HTML from format_html
return mark_safe( # noqa: S308 # NOSEMGREP: python.django.security.audit.avoid-mark-safe.avoid-mark-safe
" ".join(links)
)

def custom_field_owasp_url(self, obj):
"""Entity OWASP URL with uniform formatting."""
if not hasattr(obj, "key") or not obj.key:
return ""

return mark_safe( # noqa: S308
f"<a href='https://owasp.org/{escape(obj.key)}' target='_blank'>↗️</a>"
)
return format_html("<a href='https://owasp.org/{}' target='_blank'>↗️</a>", obj.key)

def _format_github_link(self, repository):
"""Format a single GitHub repository link."""
Expand All @@ -130,9 +128,10 @@ def _format_github_link(self, repository):
if not hasattr(repository, "key") or not repository.key:
return ""

return mark_safe( # noqa: S308
f"<a href='https://github.com/{escape(repository.owner.login)}/"
f"{escape(repository.key)}' target='_blank'>↗️</a>"
return format_html(
"<a href='https://github.com/{}/{}' target='_blank'>↗️</a>",
repository.owner.login,
repository.key,
)

custom_field_github_urls.short_description = "GitHub 🔗"
Expand Down
5 changes: 4 additions & 1 deletion backend/apps/owasp/admin/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ def render(self, name, value, attrs=None, renderer=None):
f"id='lookup_id_{name}' title='Look up related objects'></a>"
)

return mark_safe(f"{widget_html}{search_button}") # noqa: S308
# Use mark_safe since both widget_html and search_button are already HTML
return mark_safe( # noqa: S308 # NOSEMGREP: python.django.security.audit.avoid-mark-safe.avoid-mark-safe
f"{widget_html} {search_button}"
)
7 changes: 6 additions & 1 deletion backend/apps/slack/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
slack_handler = SlackRequestHandler(SlackConfig.app)


@csrf_exempt # NOSONAR
@csrf_exempt # NOSEMGREP: python.django.security.audit.csrf-exempt.no-csrf-exempt # NOSONAR
@require_POST # NOSONAR
def slack_request_handler(request):
"""Handle Slack requests.
Expand All @@ -20,5 +20,10 @@ def slack_request_handler(request):
Returns:
HttpResponse: The response generated by the SlackRequestHandler.

Note:
CSRF exemption is required for webhook endpoints. Security is ensured through
Slack's request signature verification, which is automatically handled by
slack-bolt's SlackRequestHandler when signing_secret is configured in SlackConfig.app.

"""
return slack_handler.handle(request)
3 changes: 2 additions & 1 deletion cspell/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts

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
2 changes: 2 additions & 0 deletions cspell/custom-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ navlink
nestbot
ngx
noinput
nosemgrep
nosniff
nspname
numfmt
Expand Down Expand Up @@ -147,6 +148,7 @@ saft
sakanashi
samm
schemathesis
semgrep
seo
skillstruck
slackbot
Expand Down
3 changes: 2 additions & 1 deletion docker/cspell/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts

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
1 change: 1 addition & 0 deletions docker/semgrep/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM semgrep/semgrep:1.148.0
3 changes: 2 additions & 1 deletion frontend/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ server {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
# Reason: Standard reverse proxy configuration; Host header forwarding is required for routing.
proxy_set_header Host $host; # nosemgrep: generic.nginx.security.request-host-used.request-host-used
proxy_set_header Upgrade $http_upgrade;
proxy_cache_bypass $http_upgrade;
}
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"framer-motion": "^12.29.0",
"graphql": "^16.12.0",
"ics": "^3.8.1",
"isomorphic-dompurify": "^2.35.0",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"lodash": "^4.17.23",
Expand Down
Loading
Loading