Skip to content

Commit 19eedd1

Browse files
authored
Merge branch 'main' into feature/iac-scaffolding
2 parents bca75ce + 9766b3c commit 19eedd1

File tree

185 files changed

+6551
-4907
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

185 files changed

+6551
-4907
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Check PR linked issue and assignee
2+
3+
on:
4+
pull_request:
5+
types:
6+
- opened
7+
- synchronize
8+
9+
permissions:
10+
contents: read
11+
issues: write
12+
pull-requests: write
13+
14+
jobs:
15+
check-pr-issue:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- uses: actions/checkout@v5
20+
21+
- name: Check PR linked issue and assignee
22+
uses: arkid15r/check-pr-issue-action@a3635191c798f111aae577759b579dc37bb13e02
23+
with:
24+
close_pr_on_failure: 'false'
25+
github_token: ${{ secrets.GITHUB_TOKEN }}
26+
no_assignee_message: 'Test: The linked issue must be assigned to the PR author.'
27+
no_issue_message: 'Test: This PR must be linked to an issue.'
28+
require_assignee: 'true'

.github/workflows/run-ci-cd.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
7272

7373
- name: Install pnpm
74-
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
74+
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
7575
with:
7676
version: 10
7777
run_install: true
@@ -557,7 +557,7 @@ jobs:
557557
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
558558

559559
- name: Install pnpm
560-
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
560+
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
561561
with:
562562
run_install: true
563563
version: 10

.github/workflows/run-code-ql.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ jobs:
3131
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
3232

3333
- name: Initialize CodeQL
34-
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93
34+
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5
3535
with:
3636
languages: ${{ matrix.language }}
3737

3838
- name: Install pnpm
39-
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
39+
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
4040
with:
4141
version: 10
4242
run_install: false
@@ -55,6 +55,6 @@ jobs:
5555
run: pnpm install --frozen-lockfile
5656

5757
- name: Perform CodeQL analysis
58-
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93
58+
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5
5959
with:
6060
category: /language:${{ matrix.language }}

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ repos:
1010
exclude: (.github|pnpm-lock.yaml)
1111

1212
- repo: https://github.com/astral-sh/ruff-pre-commit
13-
rev: v0.13.2
13+
rev: v0.14.0
1414
hooks:
1515
- id: ruff
1616
args:
@@ -83,7 +83,7 @@ repos:
8383
exclude: pnpm-lock.yaml
8484

8585
- repo: https://github.com/tox-dev/pyproject-fmt
86-
rev: v2.7.0
86+
rev: v2.10.0
8787
hooks:
8888
- id: pyproject-fmt
8989

LEARN.md

Lines changed: 0 additions & 73 deletions
This file was deleted.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77

88
# [OWASP Nest](https://nest.owasp.org/)
99

10-
[![OWASP](https://img.shields.io/badge/OWASP-Incubator-blue?style=for-the-badge)](https://owasp.org/www-project-nest/) [![OWASP](https://img.shields.io/badge/OWASP-Code-blue?style=for-the-badge)](https://owasp.org/www-project-nest/) [![project-nest](https://img.shields.io/badge/OWASP-%23project--nest-blue?logo=slack&logoColor=white&style=for-the-badge)](https://owasp.slack.com/messages/project-nest)
10+
[![OWASP](https://img.shields.io/badge/Lab-blue?&label=owasp%20level&style=for-the-badge)](https://owasp.org/www-project-nest/) [![OWASP](https://img.shields.io/badge/Code-blue?label=OWASP%20Type&style=for-the-badge)](https://owasp.org/www-project-nest/) [![project-nest](https://img.shields.io/badge/%23project--nest-blue?label=OWASP%20Slack&logoColor=white&style=for-the-badge)](https://owasp.slack.com/messages/project-nest)
1111

12-
[![License](https://img.shields.io/github/license/owasp/nest?color=41BE4A&label=License&style=for-the-badge)](https://github.com/OWASP/Nest/blob/main/LICENSE) [![Last Commit](https://img.shields.io/github/last-commit/owasp/nest/main?style=for-the-badge&label=Last%20commit)](https://github.com/OWASP/Nest/commits/main/) [![Contributors](https://img.shields.io/github/contributors/owasp/nest?style=for-the-badge&label=Contributors)](https://github.com/OWASP/Nest/graphs/contributors)
12+
[![License](https://img.shields.io/github/license/owasp/nest?color=blue&label=License&style=for-the-badge)](https://github.com/OWASP/Nest/blob/main/LICENSE) [![Last Commit](https://img.shields.io/github/last-commit/owasp/nest/main?color=blue&style=for-the-badge&label=Last%20commit)](https://github.com/OWASP/Nest/commits/main/) [![Contributors](https://img.shields.io/github/contributors/owasp/nest?style=for-the-badge&label=Contributors&color=blue)](https://github.com/OWASP/Nest/graphs/contributors)
1313

14-
[![CI/CD](https://img.shields.io/github/actions/workflow/status/owasp/nest/run-ci-cd.yaml?branch=main&label=Build&style=for-the-badge)](https://github.com/owasp/nest/actions/workflows/run-ci-cd.yaml?query=branch%3Amain) [![CodeQL](https://img.shields.io/github/actions/workflow/status/owasp/nest/run-code-ql.yaml?branch=main&label=CodeQL&style=for-the-badge)](https://github.com/owasp/nest/actions/workflows/run-code-ql.yaml?query=branch%3Amain) [![Sonarqube](https://img.shields.io/sonar/quality_gate/OWASP_Nest?server=https://sonarcloud.io&style=for-the-badge&label=Sonarqube)](https://sonarcloud.io/summary/new_code?id=OWASP_Nest&branch=main)
14+
[![CI/CD](https://img.shields.io/github/actions/workflow/status/owasp/nest/run-ci-cd.yaml?branch=main&color=blue&label=Build&style=for-the-badge)](https://github.com/owasp/nest/actions/workflows/run-ci-cd.yaml?query=branch%3Amain) [![CodeQL](https://img.shields.io/github/actions/workflow/status/owasp/nest/run-code-ql.yaml?branch=main&color=blue&label=CodeQL&style=for-the-badge)](https://github.com/owasp/nest/actions/workflows/run-code-ql.yaml?query=branch%3Amain) [![Sonarqube](https://img.shields.io/sonar/quality_gate/OWASP_Nest?color=blue&server=https://sonarcloud.io&style=for-the-badge&label=Sonarqube)](https://sonarcloud.io/summary/new_code?id=OWASP_Nest&branch=main)
1515

1616
[![Issues](https://img.shields.io/github/issues/owasp/nest?color=blue&style=for-the-badge&label=Issues)](https://github.com/OWASP/Nest/issues) [![Pull Requests](https://img.shields.io/github/issues-pr/owasp/nest?color=blue&style=for-the-badge&label=Pull%20Requests)](https://github.com/OWASP/Nest/pulls)
1717

backend/apps/api/decorators/__init__.py

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Decorator for API Cache."""
2+
3+
from functools import wraps
4+
from http import HTTPStatus
5+
6+
from django.conf import settings
7+
from django.core.cache import cache
8+
from django.http import HttpRequest
9+
10+
11+
def generate_key(
12+
request: HttpRequest,
13+
prefix: str,
14+
):
15+
"""Generate a cache key for a request."""
16+
return f"{prefix}:{request.get_full_path()}"
17+
18+
19+
def cache_response(
20+
ttl: int | None = None,
21+
prefix: str | None = None,
22+
):
23+
"""Cache API responses for GET and HEAD requests.
24+
25+
Args:
26+
ttl (int): The time-to-live for the cache entry in seconds.
27+
prefix (str): A prefix for the cache key.
28+
29+
"""
30+
if ttl is None:
31+
ttl = settings.API_CACHE_TIME_SECONDS
32+
33+
if prefix is None:
34+
prefix = settings.API_CACHE_PREFIX
35+
36+
def decorator(view_func):
37+
@wraps(view_func)
38+
def _wrapper(request, *args, **kwargs):
39+
if request.method not in ("GET", "HEAD"):
40+
return view_func(request, *args, **kwargs)
41+
42+
cache_key = generate_key(
43+
request=request,
44+
prefix=prefix,
45+
)
46+
if response := cache.get(cache_key):
47+
return response
48+
49+
response = view_func(request, *args, **kwargs)
50+
if response.status_code == HTTPStatus.OK:
51+
cache.set(cache_key, response, timeout=ttl)
52+
return response
53+
54+
return _wrapper
55+
56+
return decorator

backend/apps/api/rest/v0/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from django.conf import settings
44
from ninja import NinjaAPI, Swagger
5+
from ninja.pagination import RouterPaginated
56
from ninja.throttling import AuthRateThrottle
67

78
from apps.api.rest.auth.api_key import ApiKey as ApiKey
9+
from apps.api.rest.v0.chapter import router as chapter_router
810
from apps.api.rest.v0.committee import router as committee_router
911
from apps.api.rest.v0.event import router as event_router
1012
from apps.api.rest.v0.issue import router as issue_router
@@ -15,8 +17,6 @@
1517
from apps.api.rest.v0.repository import router as repository_router
1618
from apps.api.rest.v0.sponsor import router as sponsor_router
1719

18-
from .chapter import router as chapter_router
19-
2020
ROUTERS = {
2121
# Chapters.
2222
"/chapters": chapter_router,
@@ -42,11 +42,12 @@
4242

4343
api_settings = {
4444
"auth": ApiKey(), # The `api_key` param name is based on the ApiKey class name.
45+
"default_router": RouterPaginated(),
4546
"description": "Open Worldwide Application Security Project API",
4647
"docs": Swagger(settings={"persistAuthorization": True}),
4748
"throttle": [AuthRateThrottle("10/s")],
4849
"title": "OWASP Nest",
49-
"version": "0.2.3",
50+
"version": "0.2.4",
5051
}
5152

5253
api_settings_customization = {}

backend/apps/api/rest/v0/chapter.py

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,81 +4,92 @@
44
from http import HTTPStatus
55
from typing import Literal
66

7-
from django.conf import settings
87
from django.http import HttpRequest
9-
from django.views.decorators.cache import cache_page
10-
from ninja import Field, FilterSchema, Path, Query, Router, Schema
8+
from ninja import Field, FilterSchema, Path, Query, Schema
119
from ninja.decorators import decorate_view
12-
from ninja.pagination import PageNumberPagination, paginate
10+
from ninja.pagination import RouterPaginated
1311
from ninja.responses import Response
1412

15-
from apps.owasp.models.chapter import Chapter
13+
from apps.api.decorators.cache import cache_response
14+
from apps.owasp.models.chapter import Chapter as ChapterModel
1615

17-
router = Router()
16+
router = RouterPaginated(tags=["Chapters"])
1817

1918

20-
class ChapterErrorResponse(Schema):
21-
"""Chapter error response schema."""
19+
class ChapterBase(Schema):
20+
"""Base schema for Chapter (used in list endpoints)."""
2221

23-
message: str
22+
created_at: datetime
23+
key: str
24+
name: str
25+
updated_at: datetime
2426

27+
@staticmethod
28+
def resolve_key(obj):
29+
"""Resolve key."""
30+
return obj.nest_key
2531

26-
class ChapterFilterSchema(FilterSchema):
27-
"""Filter schema for Chapter."""
2832

29-
country: str | None = Field(None, description="Country of the chapter")
30-
region: str | None = Field(None, description="Region of the chapter")
33+
class Chapter(ChapterBase):
34+
"""Schema for Chapter (minimal fields for list display)."""
3135

3236

33-
class ChapterSchema(Schema):
34-
"""Schema for Chapter."""
37+
class ChapterDetail(ChapterBase):
38+
"""Detail schema for Chapter (used in single item endpoints)."""
3539

3640
country: str
37-
created_at: datetime
38-
name: str
3941
region: str
40-
updated_at: datetime
42+
43+
44+
class ChapterError(Schema):
45+
"""Chapter error schema."""
46+
47+
message: str
48+
49+
50+
class ChapterFilter(FilterSchema):
51+
"""Filter for Chapter."""
52+
53+
country: str | None = Field(None, description="Country of the chapter")
4154

4255

4356
@router.get(
4457
"/",
4558
description="Retrieve a paginated list of OWASP chapters.",
4659
operation_id="list_chapters",
47-
response={200: list[ChapterSchema]},
60+
response=list[Chapter],
4861
summary="List chapters",
49-
tags=["Chapters"],
5062
)
51-
@decorate_view(cache_page(settings.API_CACHE_TIME_SECONDS))
52-
@paginate(PageNumberPagination, page_size=settings.API_PAGE_SIZE)
63+
@decorate_view(cache_response())
5364
def list_chapters(
5465
request: HttpRequest,
55-
filters: ChapterFilterSchema = Query(...),
66+
filters: ChapterFilter = Query(...),
5667
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
5768
None,
5869
description="Ordering field",
5970
),
60-
) -> list[ChapterSchema]:
71+
) -> list[Chapter]:
6172
"""Get chapters."""
62-
return filters.filter(Chapter.active_chapters.order_by(ordering or "-created_at"))
73+
return filters.filter(ChapterModel.active_chapters.order_by(ordering or "-created_at"))
6374

6475

6576
@router.get(
6677
"/{str:chapter_id}",
6778
description="Retrieve chapter details.",
6879
operation_id="get_chapter",
6980
response={
70-
HTTPStatus.NOT_FOUND: ChapterErrorResponse,
71-
HTTPStatus.OK: ChapterSchema,
81+
HTTPStatus.NOT_FOUND: ChapterError,
82+
HTTPStatus.OK: ChapterDetail,
7283
},
7384
summary="Get chapter",
74-
tags=["Chapters"],
7585
)
86+
@decorate_view(cache_response())
7687
def get_chapter(
7788
request: HttpRequest,
7889
chapter_id: str = Path(example="London"),
79-
) -> ChapterSchema | ChapterErrorResponse:
90+
) -> ChapterDetail | ChapterError:
8091
"""Get chapter."""
81-
if chapter := Chapter.active_chapters.filter(
92+
if chapter := ChapterModel.active_chapters.filter(
8293
key__iexact=(
8394
chapter_id if chapter_id.startswith("www-chapter-") else f"www-chapter-{chapter_id}"
8495
)

0 commit comments

Comments
 (0)