Skip to content
Closed
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
33 changes: 0 additions & 33 deletions .github/workflows/scan-path-context-coverage.yml

This file was deleted.

4 changes: 0 additions & 4 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,3 @@
## 2024-05-19 - Pathlib Instantiation in Hot Loops
**Learning:** Blindly instantiating `pathlib.Path` objects in hot loops (like file discovery loops or display formatters such as `detect_language_axes` and `_display_path`) creates measurable performance bottlenecks due to object allocation and potential system calls. When checking file extensions or processing path strings, Python's native string methods like `str.rfind()` and `str.replace()` are vastly more efficient.
**Action:** Replace `pathlib.Path` usage with fast C-level string operations (`replace("\\", "/")`, `rfind()`, `split()`) in performance-critical areas, particularly when traversing thousands of files, formatting paths, or extracting file extensions.

## 2024-11-20 - Optimize multiple tuple generation from a single collection
**Learning:** `build_rule_metadata` derives exactly two collections, `owasp` and `cwe`, from the same references. Replacing its two generator traversals with one explicit loop reduces element visits from about 2N to N. Both versions remain O(N), so this is a constant-factor optimization rather than an asymptotic complexity improvement.
**Action:** Combine repeated traversal when fixed derived collections share one source, while preserving ordering and classification semantics. Benchmark the production hot path before claiming a material wall-clock improvement.
18 changes: 3 additions & 15 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,6 @@
**Learning:** Users who heavily rely on keyboard navigation (and power users) experience friction when forced to backspace manually or switch to the mouse to click a "Clear" button after filtering a list.
**Action:** Always provide an `Escape` key listener on search inputs to instantly clear the query and re-render the view, matching native OS text field behavior.

## 2026-08-10 - Keyboard Accessible CSS Charts
**Learning:** DOM-based CSS charts (like bar graphs using styled `<div>` elements) are inherently inaccessible to keyboard and screen reader users unless explicitly configured. Without focus management and roles, interactive or informative charts become invisible to assistive technologies.
**Action:** Always add `tabindex="0"`, `role="img"`, an explicit `aria-label`, and a `:focus-visible` outline to individual chart elements so keyboard users can navigate them and screen readers can announce their data points.

## 2026-08-06 - Interactive Dashboard Cards for Quick Filtering
**Learning:** Making metric cards (like severity counts) interactive significantly reduces friction compared to using dropdown filters. It's a common dashboard pattern that users intuitively try to click, and explicitly adding `role="button"`, `tabindex="0"`, and `aria-pressed` makes it accessible.
**Action:** When aggregate metric cards act as filter toggles, give them an accessible name that includes the current count, expose `role="button"`, `tabindex="0"`, and `aria-pressed`, and handle both `Enter` and `Space` activation while preventing the Space key's default scrolling.

## 2026-08-12 - Async Detail Focus Restoration
**Learning:** Closing an asynchronous detail panel without invalidating the pending request lets a late response repopulate the panel or steal focus after the user has left it.
**Action:** Invalidate the request generation on close, restore focus only to a connected trigger, and expose equivalent Escape and close-button paths for success and error states.

## 2026-08-12 - Skip to Content Accessibility
**Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load.
**Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline.
## 2026-08-07 - Refactoring inline handlers to Event Listeners for CSP Compliance
**Learning:** Hard-coded `onclick` attributes in plain HTML UI viewers (like `scanner/dashboard/index.html`) break Content Security Policy (CSP) guidelines because they execute inline scripts. Relying on them creates vulnerabilities and prevents deployment in strict security contexts. Additionally, missing `<button>` styling can make file inputs look jarring.
**Action:** Always avoid `onclick` handlers in HTML. Extract interactivity to a `<script>` block and bind behaviors using `addEventListener`. For unstylable elements like `<input type="file">`, visually hide them with `class="sr-only" tabindex="-1" aria-hidden="true"` and use a styled proxy `<button>` with an `addEventListener` that triggers the hidden input's click event.
5 changes: 0 additions & 5 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,3 @@
**Vulnerability:** DOM XSS via unescaped `severity` string interpolated into `innerHTML` in `scanner/dashboard/index.html`.
**Learning:** Even enum-like or seemingly safe meta-fields like `severity` can contain malicious payloads if sourced from user input (findings file) and directly injected into innerHTML.
**Prevention:** Always use the `esc()` sanitizer for any dynamically rendered property from `findings.json`, regardless of expected schema types.

## 2025-02-28 - Stored SSRF and Unhandled Parsing Exceptions Guardrail
**Vulnerability:** The `/api/v1/webhook` POST endpoint in `appguardrail_core/controlplane.py` failed to validate the `url` property when accepting it into the database, leading to Stored SSRF risks. In addition, the core SSRF validation logic (`_is_safe_url`) in both the CLI and control-plane did not verify the input type (e.g. `isinstance(url, str)`). Passing non-string types (like integers) resulted in unhandled `AttributeError` exceptions inside `urllib.parse.urlparse`, which led to API 500 crashes on malicious JSON payloads.
**Learning:** Network endpoints must explicitly validate the data type of user-provided configurations prior to execution or storage. Furthermore, webhooks configured by users should always be checked for SSRF when saved, as trusting them later assumes input has already been safely validated, bypassing downstream network guardrails.
**Prevention:** Apply `_is_safe_url` checks directly upon ingestion (e.g., in `/api/v1/webhook`) and enforce type checks `if not isinstance(url, str): return False` prior to using library parsing functions like `urlparse`. Always return gracefully failing responses (like `400 Bad Request`) for unsafe URLs instead of allowing unhandled 500 server errors.
6 changes: 0 additions & 6 deletions CHANGELOG.d/893-scan-path-context.md

This file was deleted.

3 changes: 0 additions & 3 deletions appguardrail_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@
extract_public_references,
validate_rule_metadata,
)
from appguardrail_core.scan_paths import ScanPathContext, build_scan_path_context


ReportContext = _reports.ReportContext
Expand Down Expand Up @@ -188,7 +187,6 @@ def render_buyer_diligence_report(
"SchemaInspection",
"SchemaMigrationError",
"SchemaMigrationResult",
"ScanPathContext",
"StackProfile",
"StalePurgePreview",
"build_buyer_evidence_pack",
Expand All @@ -197,7 +195,6 @@ def render_buyer_diligence_report(
"build_org_inventory",
"build_purge_preview",
"build_rule_metadata",
"build_scan_path_context",
"buyer_evidence_pack_to_dict",
"classify_pr_gate",
"collect_openssf_evidence",
Expand Down
26 changes: 13 additions & 13 deletions appguardrail_core/code_scanning.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,19 +237,19 @@ def compare_snapshots(
return DriftAssessment(status="unknown", reason="no_healthy_base_analysis")

current_by_identity = {evidence.identity: evidence for evidence in current.analyses}

missing_list, errored_list, warnings_list = [], [], []
for identity in sorted(healthy_base):
if identity not in current_by_identity:
missing_list.append(identity)
else:
current_evidence = current_by_identity[identity]
if not current_evidence.healthy:
errored_list.append(current_evidence)
if current_evidence.warning:
warnings_list.append(current_evidence)
missing, errored, warnings = tuple(missing_list), tuple(errored_list), tuple(warnings_list)

missing = tuple(
identity for identity in sorted(healthy_base) if identity not in current_by_identity
)
errored = tuple(
current_by_identity[identity]
for identity in sorted(healthy_base)
if identity in current_by_identity and not current_by_identity[identity].healthy
)
warnings = tuple(
current_by_identity[identity]
for identity in sorted(healthy_base)
if identity in current_by_identity and current_by_identity[identity].warning
)
if missing or errored:
return DriftAssessment(
status="drift",
Expand Down
17 changes: 4 additions & 13 deletions appguardrail_core/controlplane.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,6 @@ def _is_safe_url(url: str) -> bool:
import urllib.parse
import socket

if not isinstance(url, str):
return False

try:
parsed = urllib.parse.urlparse(
url
Expand Down Expand Up @@ -615,10 +612,7 @@ def _body(self):
# Negative reads until EOF; oversized bodies exhaust memory.
return None
try:
raw_body = self.rfile.read(length)
if not raw_body:
return None
return json.loads(raw_body)
return json.loads(self.rfile.read(length) or b"{}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿ—„๏ธ Data Integrity & Integration | ๐ŸŸ  Major | โšก Quick win

๋นˆ ๋ณธ๋ฌธ์„ {}๋กœ ๋ณ€ํ™˜ํ•˜์ง€ ๋งˆ์„ธ์š”.

_body๋Š” ์—ฌ๋Ÿฌ POST ํ•ธ๋“ค๋Ÿฌ์—์„œ ๊ณต์œ ๋ฉ๋‹ˆ๋‹ค. Content-Length: 0์ด๋ฉด /api/v1/webhook์ด set_webhook(conn, org, None)์„ ํ˜ธ์ถœํ•˜์—ฌ ์ €์žฅ๋œ webhook_url์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. /api/v1/keys๋„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ create_key๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„๋ฝ๋œ ๋ณธ๋ฌธ์ด ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

๋นˆ ๋ณธ๋ฌธ์€ None์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ๊ฐ ํ•ธ๋“ค๋Ÿฌ์—์„œ ํ•„์š”ํ•œ ๊ฐ์ฒด์™€ ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๊ฒ€์ฆํ•˜์„ธ์š”. ์›นํ›… ์‚ญ์ œ๋ฅผ ์ง€์›ํ•˜๋ฉด ๋นˆ ๋ณธ๋ฌธ์ด ์•„๋‹ˆ๋ผ {"url": null} ๊ฐ™์€ ๋ช…์‹œ์  ์š”์ฒญ๋งŒ ํ—ˆ์šฉํ•˜์„ธ์š”.

As per coding guidelines, โ€œValidate request bodies, parameters, queries, uploaded files, and webhook payloads server-side.โ€

์ˆ˜์ • ์˜ˆ์‹œ
-                return json.loads(self.rfile.read(length) or b"{}")
+                if length == 0:
+                    return None
+                return json.loads(self.rfile.read(length))
๐Ÿค– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@appguardrail_core/controlplane.py` at line 615, Update the shared _body
method to return None when Content-Length is zero or the request body is empty
instead of defaulting to {}. Add explicit body/object and required-field
validation in each affected POST handler, including the webhook and key creation
handlers, and only permit webhook deletion through an explicit payload such as
{"url": null}.

Source: Coding guidelines

except (ValueError, TypeError):
return None

Expand All @@ -635,13 +629,10 @@ def do_POST(self):
if not has_role(role, "owner"):
return self._json(403, {"error": "owner role required"})
body = self._body()
if body is None or not isinstance(body, dict):
if body is None:
return self._json(400, {"error": "invalid JSON body"})
webhook_url = body.get("url")
if webhook_url is not None and not _is_safe_url(webhook_url):
return self._json(400, {"error": "invalid webhook url"})
set_webhook(conn, org, webhook_url)
return self._json(200, {"webhook_url": webhook_url})
set_webhook(conn, org, (body or {}).get("url"))
return self._json(200, {"webhook_url": (body or {}).get("url")})

if path == "/api/v1/keys":
if not has_role(role, "owner"):
Expand Down
Loading
Loading