Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,23 @@ release tag.

## [Unreleased]

## [1.1.0] – 2026-08-20
## [1.1.0] – 2026-09-27

### Added

- **Bilingual web UI (German/English)**: a language switch (DE/EN) in the header
of the web UI, persisted per browser (`localStorage`). Switching language
re-renders the already-loaded scan results without starting a new scan.
- Covers the entire interface: all labels, tooltips, tabs, buttons, status
messages and the Help/FAQ panel.
- Covers scan output: findings, the ASVS 5.0 report, compliance mapping
(SOC 2/ISO 27001), the legal/accessibility report, website analysis,
DNS/registrar checks and technology detection all carry an English
translation alongside the German original, selected via `?lang=de|en` on
the JSON API and included in the HTML/SARIF/evidence exports.
- German remains the default and the source of truth; English is an
additive translation layer and does not change any existing check logic,
severities or German wording.
- **Markup/standards analysis** (`scanner/markup.py`): new category in the
`--analyze` report that checks the delivered start page against the most common
HTML standard violations – doctype/quirks mode, unclosed and stray tags, invalid
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ An expandable **"Help / FAQ"** panel (with a live filter) explains every check
directly in the interface. The same content is available on the CLI via
`python scan.py --faq`.

A **DE/EN language switch** in the header toggles the entire interface,
including scan findings, the ASVS/legal/analysis reports and the FAQ, between
German and English; the choice is remembered per browser.

> Note: reachable only locally by default. Only bind the server to another address
> (`--host`) if you knowingly accept the access risks.

Expand Down Expand Up @@ -944,7 +948,7 @@ stamped into every export – JSON (`tool.version`), HTML (report header), SARIF
So an audit finding can always be traced back to the exact release that produced it.

```powershell
python scan.py --version # web-vuln-scanner 1.0.0
python scan.py --version # web-vuln-scanner 1.1.0
```

Installation from a release: download the source archive of the desired tag (or
Expand Down
51 changes: 46 additions & 5 deletions scanner/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,14 @@ def _probe_privileged(session, anon, base_url, authed) -> list[Finding]:
"Der Pfad liefert ohne jede Session sensiblen/administrativen Inhalt - die "
"Authentifizierung fehlt oder greift nicht.",
url, evidence=f"anonym HTTP {r_anon.status_code}, sensibler Inhalt",
recommendation="Authentifizierung & Autorisierung serverseitig erzwingen."))
recommendation="Authentifizierung & Autorisierung serverseitig erzwingen.",
en={
"title": f"Privileged area reachable without authentication: {path}",
"description": "The path serves sensitive/administrative content without any "
"session - authentication is missing or not enforced.",
"evidence": f"anonymous HTTP {r_anon.status_code}, sensitive content",
"recommendation": "Enforce authentication & authorization server-side.",
}))
continue

# Mit aktueller Session erreichbar (anonym aber nicht) -> Rolle pruefen.
Expand All @@ -123,7 +130,16 @@ def _probe_privileged(session, anon, base_url, authed) -> list[Finding]:
"Session NICHT die erforderliche (Admin-)Rolle hat, liegt eine vertikale "
"Rechteausweitung vor - bitte die Rolle manuell verifizieren.",
url, evidence=f"mit Session HTTP {r_auth.status_code}; anonym blockiert",
recommendation="Funktions-/objektbezogene Autorisierung pro Rolle pruefen."))
recommendation="Funktions-/objektbezogene Autorisierung pro Rolle pruefen.",
en={
"title": f"Privileged area reachable with current session: {path}",
"description": "The path is reachable with the session used (not anonymously). "
"If this session does NOT have the required (admin) role, this "
"is a vertical privilege escalation - please verify the role "
"manually.",
"evidence": f"with session HTTP {r_auth.status_code}; anonymous blocked",
"recommendation": "Check function-/object-level authorization per role.",
}))
continue

# Mit Session gesperrt (401/403) -> Bypass-Versuche.
Expand All @@ -145,7 +161,15 @@ def _try_bypass(session, url, path, fb) -> list[Finding]:
"gesperrten Inhalt.",
v, evidence="Variante -> HTTP 200 statt 401/403",
recommendation="Autorisierung nach Pfad-Normalisierung pruefen; nicht auf "
"Pfad-Schreibweise verlassen.")]
"Pfad-Schreibweise verlassen.",
en={
"title": f"403/401 bypass possible (path variant): {path}",
"description": "An altered path spelling bypasses the access block and returns "
"the blocked content.",
"evidence": "Variant -> HTTP 200 instead of 401/403",
"recommendation": "Check authorization after path normalization; do not rely on "
"path spelling.",
})]
# Header-Varianten.
for tmpl in _BYPASS_HEADERS:
hdr = {k: v.format(path=path) for k, v in tmpl.items()}
Expand All @@ -156,7 +180,15 @@ def _try_bypass(session, url, path, fb) -> list[Finding]:
f"Der Header {list(hdr)[0]} umgeht die Zugriffssperre und liefert den gesperrten Inhalt.",
url, evidence=f"{list(hdr.items())[0]} -> HTTP 200 statt 401/403",
recommendation="Solche Override-/Forwarding-Header serverseitig ignorieren und "
"Autorisierung unabhaengig davon erzwingen.")]
"Autorisierung unabhaengig davon erzwingen.",
en={
"title": f"403/401 bypass possible (header): {path}",
"description": f"The header {list(hdr)[0]} bypasses the access block and returns "
"the blocked content.",
"evidence": f"{list(hdr.items())[0]} -> HTTP 200 instead of 401/403",
"recommendation": "Ignore such override/forwarding headers server-side and enforce "
"authorization independent of them.",
})]
return []


Expand Down Expand Up @@ -200,7 +232,16 @@ def _probe_idor(session, target: str) -> list[Finding]:
"Zugriffskontroll-Fehler vor (manuell bestaetigen).",
url, evidence=f"{param}={orig} und {param}={cand} liefern beide HTTP 200 (verschieden)",
recommendation="Objekt-Zugriffe serverseitig an den Eigentuemer/die Rolle binden "
"(nicht nur per ID adressieren)."))
"(nicht nur per ID adressieren).",
en={
"title": f"Possible IDOR via parameter '{param}'",
"description": f"Changing '{param}' ({orig} -> {cand}) returns a different, "
"valid object. If it belongs to another user, this is a "
"horizontal access control flaw (verify manually).",
"evidence": f"{param}={orig} and {param}={cand} both return HTTP 200 (different)",
"recommendation": "Bind object access server-side to the owner/role (not just "
"by ID).",
}))
return out # ein klarer Hinweis genuegt
return out

Expand Down
Loading
Loading