Skip to main content

Django scanner

shadowaudit's Django scanner reads urls.py files and extracts route definitions from path(), re_path(), url(), and router.register() calls.

How it works

The scanner finds all urls.py and *_urls.py files in the scanned directory and uses regex to extract route patterns. It converts Django's <type:param> syntax to OpenAPI's {param} syntax for comparison.

Detected patterns

Pattern 1 — path() function

from django.urls import path
from . import views

urlpatterns = [
path('users/', views.UserList),
path('users/<int:pk>/', views.UserDetail),
]

Django path parameters are converted to OpenAPI syntax:

DjangoOpenAPI (stored)
<int:pk>{pk}
<str:name>{name}
<slug:slug>{slug}
<uuid:id>{id}

Pattern 2 — re_path() / url() with regex

from django.urls import re_path

urlpatterns = [
re_path(r'^api/items/$', views.ItemList),
]
# → GET /api/items/ (regex anchors stripped)

Pattern 3 — DRF router.register()

from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet

router = DefaultRouter()
router.register(r'articles', ArticleViewSet)

This generates 6 standard REST routes automatically:

GET /articles/ — list
POST /articles/ — create
GET /articles/{pk}/ — retrieve
PUT /articles/{pk}/ — update
PATCH /articles/{pk}/ — partial_update
DELETE /articles/{pk}/ — destroy

Example scan output

shadowaudit --dir ./src --spec openapi.json --framework django
[INFO] Running Django route scanner...
[✓] Found 12 Django routes in ./src
[INFO] GET /articles/ → urls.py:7 [no auth]
[INFO] POST /articles/ → urls.py:7 [no auth]
[INFO] GET /articles/{pk}/ → urls.py:7 [no auth]
[INFO] GET /health/ → urls.py:10 [no auth]
[INFO] GET /api/users/ → urls.py:12 [auth]

Auth detection

shadowaudit detects Django auth by scanning the urls.py file content for hardcoded patterns:

@login_required @permission_required IsAuthenticated IsAdminUser
TokenAuthentication SessionAuthentication permission_classes authentication_classes
LoginRequiredMixin PermissionRequiredMixin jwt_required requires_auth

Known limitations

  • Cross-file auth detection — auth declared in views.py is not detected when scanning urls.py (tracked in the roadmap)
  • include('app.urls') prefixes — not yet reconciled (tracked in issue #1)
  • Method detection — Django path() doesn't bind to a single HTTP method. Without inspecting the view class, routes store method as 'GET' as a default. DRF ViewSets get all 6 standard methods.
  • Class-based view methods — not yet detected (would need to inspect def get, def post on the view class)