You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by as...@apache.org on 2026/04/22 05:21:19 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 16.3.2 (logging_and_monitoring)

This is an automated email from the ASF dual-hosted git repository.

asf-tooling pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new 9f9029a  ASVS L3 audit: 16.3.2 (logging_and_monitoring)
9f9029a is described below

commit 9f9029a1dfae5376d3c222dce5ff7c7eb5b493ae
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 07:21:14 2026 +0200

    ASVS L3 audit: 16.3.2 (logging_and_monitoring)
---
 .../v3/d0aa7e9/logging_and_monitoring/16.3.2.md    | 542 +++++++++++++++++++++
 1 file changed, 542 insertions(+)

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/logging_and_monitoring/16.3.2.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/logging_and_monitoring/16.3.2.md
new file mode 100644
index 0000000..00e0ea8
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/logging_and_monitoring/16.3.2.md
@@ -0,0 +1,542 @@
+# ASVS 16.3.2 Security Audit Report
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 22, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 16.3.2 — Verify that failed authorization attempts are logged. For L3, this must include logging all authorization decisions, including logging when sensitive data is accessed (without logging the sensitive data itself).
+- **Files analyzed:** 4 relevant out of 4 total, 0 skipped
+
+### Finding Counts
+
+| Severity | Count |
+|----------|-------|
+| Critical | 0 |
+| High | 4 |
+| Medium | 1 |
+| Low | 0 |
+| **Total** | **5** |
+
+---
+
+## Security Controls Inventory
+
+| Control | Defined In | Type | Coverage |
+|---------|-----------|------|----------|
+| Authentication decorator | `asfquart.auth.require` | Framework | Applied to all protected endpoints ✓ |
+| Voter eligibility check (mayvote) | `pages.py:vote_on_page` ~L241 | Application | Applied at vote-on page only |
+| Document access check (mayvote) | `pages.py:serve_doc` ~L496 | Application | Applied at doc serving only |
+| Election state assertions | `election.py` (multiple) | Library | Applied to state-changing operations ✓ |
+| Action audit logging (success) | `pages.py` (multiple endpoints) | Application | Applied to successful mutations ✓ |
+| **Authorization failure logging** | **NOT DEFINED** | **MISSING** | **Not applied anywhere** |
+| **Tally audit logging** | **NOT DEFINED** | **MISSING** | **Not applied** |
+
+---
+
+## Critical File Review
+
+### `v3/server/pages.py` — Authorization Logging Coverage
+
+| Endpoint | Auth Check | Auth Failure Logged | Auth Success Logged | Action Logged |
+|----------|-----------|-------------------|-------------------|--------------|
+| `GET /voter` | Framework only | N/A (framework) | No | No |
+| `GET /vote-on/<eid>` | mayvote query | **NO** ❌ | No | No |
+| `POST /do-vote/<eid>` | **MISSING** ❌ | N/A | No | Yes ✓ |
+| `GET /do-open/<eid>` | **MISSING** ❌ | N/A | No | Yes ✓ |
+| `GET /do-close/<eid>` | **MISSING** ❌ | N/A | No | Yes ✓ |
+| `POST /do-add-issue/<eid>` | **MISSING** ❌ | N/A | No | Yes ✓ |
+| `POST /do-edit-issue/<eid>/<iid>` | **MISSING** ❌ | N/A | No | Yes ✓ |
+| `POST /do-delete-issue/<eid>/<iid>` | **MISSING** ❌ | N/A | No | Yes ✓ |
+| `POST /do-create-election` | Framework (PMC) | N/A (framework) | No | Yes ✓ |
+| `GET /docs/<iid>/<docname>` | mayvote query | **NO** ❌ | No | No |
+| `GET /manage/<eid>` | **MISSING** ❌ | N/A | No | No |
+
+### `v3/server/bin/tally.py` — Audit Logging Coverage
+
+| Operation | Logged | Details |
+|-----------|--------|---------|
+| Tally initiation | **NO** ❌ | No record of who/when |
+| Election selection | **NO** ❌ | No record of which election |
+| Vote decryption | **NO** ❌ | Most sensitive operation |
+| Tally completion | **NO** ❌ | No completion record |
+| Tampering detected | **print() only** ⚠️ | Not through logging framework |
+| Spy-on-open usage | **NO** ❌ | No record of open election access |
+
+---
+
+## Findings
+
+### High Severity
+
+#### ASVS-1632-HIGH-001
+
+**Title:** Failed authorization attempts in `vote_on_page` not logged
+
+**Location:**
+- **File:** `v3/server/pages.py`
+- **Function:** `vote_on_page`
+- **Lines:** ~241-247
+
+**Related Functions Checked:**
+- `vote_on_page` (authorization check present, logging absent)
+- `basic_info` (user context retrieval)
+- `election.q_find_issues.perform` (mayvote query)
+
+**Description:**
+
+The `vote_on_page` endpoint correctly implements an authorization check by querying the `mayvote` table to verify voter eligibility for a specific election. However, when this authorization check fails, the denial is not logged. This represents a Type B gap — the authorization control exists and is called, but the authorization failure decision is not logged.
+
+**Vulnerable Code:**
+
+```python
+# Check if user can vote in this election (has mayvote entries)
+election.q_find_issues.perform(result.uid, election.eid)
+if not election.q_find_issues.fetchall():
+    result = await basic_info()
+    result.title = 'Access Denied'
+    result.eid = election.eid
+    raise_404(T_BAD_EID, result)  # Reuse template for consistency
+```
+
+**Data Flow:**
+1. Source: Authenticated user requests `GET /vote-on/<eid>`
+2. Authorization decision: Query `mayvote` table to check eligibility
+3. Missing control: No `_LOGGER.warning()` or `_LOGGER.info()` call when authorization fails
+4. Sink: 404 response returned silently
+
+**Proof of Concept:**
+
+An authenticated committer who is NOT eligible to vote on a specific election visits `/vote-on/SOME_EID`. The system correctly denies access with a 404, but no log entry is generated. An attacker probing election IDs to find accessible elections leaves no trace.
+
+**Impact:**
+
+Failed authorization attempts to access elections are invisible to security monitoring. An attacker systematically probing election access would not be detected through log analysis.
+
+**Remediation:**
+
+```python
+# Check if user can vote in this election (has mayvote entries)
+election.q_find_issues.perform(result.uid, election.eid)
+if not election.q_find_issues.fetchall():
+    _LOGGER.warning(
+        f'Authorization failure: User[U:{result.uid}] denied access'
+        f' to election[E:{election.eid}] - not in mayvote'
+    )
+    result = await basic_info()
+    result.title = 'Access Denied'
+    result.eid = election.eid
+    raise_404(T_BAD_EID, result)
+```
+
+---
+
+#### ASVS-1632-HIGH-002
+
+**Title:** Failed authorization attempts in `serve_doc` not logged
+
+**Location:**
+- **File:** `v3/server/pages.py`
+- **Function:** `serve_doc`
+- **Lines:** ~494-499
+
+**Related Functions Checked:**
+- `serve_doc` (authorization check present, logging absent)
+- `basic_info` (user context retrieval)
+- `db.q_get_mayvote.first_row` (mayvote query)
+
+**Description:**
+
+The `serve_doc` endpoint implements authorization by querying the `mayvote` table to verify that a user has access to election issue documents. However, when this authorization check fails, the denial is not logged. This represents a Type B gap — the authorization control exists and is called, but the authorization failure decision is not logged.
+
+**Vulnerable Code:**
+
+```python
+async def serve_doc(iid, docname):
+    result = await basic_info()  # get PID
+
+    db = steve.election.Election.open_database(DB_FNAME)
+    row = db.q_get_mayvote.first_row(result.uid, iid)
+    if not row:
+        # Nothing fancy. Just pretend the file does not exist
+        quart.abort(404)
+        # NOTREACHED
+```
+
+**Data Flow:**
+1. Source: Authenticated user requests `GET /docs/<iid>/<docname>`
+2. Authorization decision: Query `mayvote` to verify access to issue documents
+3. Missing control: No logging when authorization fails
+4. Sink: 404 response returned silently
+
+**Proof of Concept:**
+
+An authenticated user tries to access supporting documents for an election issue they're not eligible to vote on: `GET /docs/SOME_IID/confidential.pdf`. Access is denied but the attempt is not logged.
+
+**Impact:**
+
+Attempts to access restricted election documents (which may contain sensitive information about candidates or proposals) leave no audit trail. This undermines the ability to detect reconnaissance or unauthorized access attempts against election materials.
+
+**Remediation:**
+
+```python
+async def serve_doc(iid, docname):
+    result = await basic_info()
+
+    db = steve.election.Election.open_database(DB_FNAME)
+    row = db.q_get_mayvote.first_row(result.uid, iid)
+    if not row:
+        _LOGGER.warning(
+            f'Authorization failure: User[U:{result.uid}] denied access'
+            f' to document for issue[I:{iid}]'
+        )
+        quart.abort(404)
+
+    _LOGGER.info(
+        f'User[U:{result.uid}] accessed document "{docname}"'
+        f' for issue[I:{iid}]'
+    )
+    return await quart.send_from_directory(DOCSDIR / iid, docname)
+```
+
+---
+
+#### ASVS-1632-HIGH-003
+
+**Title:** Tally operations create no audit trail — sensitive vote data access is unlogged
+
+**Location:**
+- **File:** `v3/server/bin/tally.py`
+- **Functions:** `main` and `tally_election`
+- **Lines:** ~120-150 and ~85-115
+
+**Related Functions Checked:**
+- `main` (tally initiation, no logging)
+- `tally_election` (vote decryption, no logging)
+- `steve.election.Election.tally_issue` (in `election.py` ~L257, no logging)
+
+**Description:**
+
+The tally script performs the most sensitive operation in the system — decrypting all encrypted votes to compute election results. Despite this, the entire tally execution path contains zero audit logging. This represents a Type A gap — no logging control exists at all for this critical operation.
+
+**Vulnerable Code:**
+
+```python
+def main(spy_on_open, election_id, issue_id, db_fname, output_format):
+    # ... election selection ...
+    election = steve.election.Election(db_fname, election_id)
+
+    # Check for tampering
+    pdb = steve.persondb.PersonDB.open(db_fname)
+    if election.is_tampered(pdb):
+        print(f'Error: Election {election_id} has been tampered with. Cannot proceed.')
+        sys.exit(1)
+
+    # Proceed with tally — NO LOGGING OF WHO TALLIED WHAT
+    tally_election(election, issue_id, output_format)
+
+
+def tally_election(election, issue_id, output_format):
+    # ... tallies all issues, decrypts all votes ...
+    # NO _LOGGER calls anywhere in this function
+    for issue in issues:
+        try:
+            tally_result, issue_voters = election.tally_issue(issue.iid)
+            # ...
+        except Exception as e:
+            print(f'Error tallying issue {issue.iid}: {e}')  # print, not _LOGGER
+            raise
+```
+
+**Data Flow:**
+1. Source: CLI user executes `tally.py` with election/issue arguments
+2. Sensitive operation: `election.tally_issue()` decrypts all votes for the election
+3. Missing control: Zero `_LOGGER` calls in the entire tally execution path
+4. The tally function in `election.py` (`tally_issue`, line ~257) also has no logging of the decryption operation
+
+**Proof of Concept:**
+
+An administrator runs `./tally.py --election-id SOME_EID --spy-on-open-elections`. All votes in an open election are decrypted and displayed. No log record exists of who accessed this data, when, or which election was tallied.
+
+**Impact:**
+
+The tallying operation is the single most sensitive operation in the system — it decrypts all encrypted votes, revealing vote content. Without audit logging:
+- There is no record of who initiated tallying
+- There is no record of when votes were decrypted
+- There is no record of whether `--spy-on-open-elections` was used to tally an election that hasn't closed yet
+- The voter list (`all_voters`) is also extracted without logging
+- For L3 compliance, all access to sensitive data must be logged
+
+**Remediation:**
+
+```python
+def main(spy_on_open, election_id, issue_id, db_fname, output_format):
+    import os
+    invoking_user = os.environ.get('USER', 'unknown')
+
+    # ... election selection ...
+
+    _LOGGER.info(
+        f'Tally initiated by system user "{invoking_user}"'
+        f' for election[E:{election_id}]'
+        f' spy_on_open={spy_on_open}'
+    )
+
+    election = steve.election.Election(db_fname, election_id)
+
+    pdb = steve.persondb.PersonDB.open(db_fname)
+    if election.is_tampered(pdb):
+        _LOGGER.critical(
+            f'TAMPERING DETECTED: election[E:{election_id}]'
+            f' — tally aborted by user "{invoking_user}"'
+        )
+        sys.exit(1)
+
+    tally_election(election, issue_id, output_format)
+
+    _LOGGER.info(
+        f'Tally completed for election[E:{election_id}]'
+        f' by system user "{invoking_user}"'
+    )
+
+
+def tally_election(election, issue_id, output_format):
+    issues = election.list_issues()
+    # ...
+    for issue in issues:
+        _LOGGER.info(
+            f'Tallying (decrypting votes) for issue[I:{issue.iid}]'
+            f' in election[E:{election.eid}]'
+        )
+        # ...
+```
+
+---
+
+#### ASVS-1632-HIGH-004
+
+**Title:** Election management endpoints lack authorization checks — no authorization decisions to log
+
+**Location:**
+- **File:** `v3/server/pages.py`
+- **Multiple endpoints** (see table below)
+
+**Affected Endpoints:**
+
+| Endpoint | Function | Line | Operation |
+|----------|----------|------|-----------|
+| `GET /do-open/<eid>` | `do_open_endpoint` | ~435 | Opens election (any committer) |
+| `GET /do-close/<eid>` | `do_close_endpoint` | ~455 | Closes election (any committer) |
+| `POST /do-vote/<eid>` | `do_vote_endpoint` | ~391 | Submits votes (any committer) |
+| `POST /do-add-issue/<eid>` | `do_add_issue_endpoint` | ~473 | Adds issue (any committer) |
+| `POST /do-edit-issue/<eid>/<iid>` | `do_edit_issue_endpoint` | ~493 | Edits issue (any committer) |
+| `POST /do-delete-issue/<eid>/<iid>` | `do_delete_issue_endpoint` | ~515 | Deletes issue (any committer) |
+| `POST /do-set-open_at/<eid>` | `do_set_open_at_endpoint` | ~376 | Sets open date (any committer) |
+| `POST /do-set-close_at/<eid>` | `do_set_close_at_endpoint` | ~382 | Sets close date (any committer) |
+| `GET /manage/<eid>` | `manage_page` | ~331 | Views management page (any committer) |
+
+**Related Functions Checked:**
+- `load_election` decorator (contains `### check authz` TODO, no implementation)
+- All management endpoints listed above (all have `### check authz` comments but no authorization logic)
+
+**Description:**
+
+Multiple election management endpoints contain `### check authz` comments indicating that authorization checks are planned but not implemented. This represents a Type A gap — no authorization control exists for fine-grained access (e.g., election ownership verification), therefore no authorization decisions exist to be logged.
+
+**Representative Vulnerable Code (`do_open_endpoint`):**
+
+```python
+@APP.get('/do-open/<eid>')
+@asfquart.auth.require({R.committer})  ### need general solution
+@load_election
+async def do_open_endpoint(election):
+    result = await basic_info()
+    ### check authz          # <-- AUTHORIZATION NOT IMPLEMENTED
+    pdb = steve.persondb.PersonDB.open(DB_FNAME)
+    election.open(pdb)       # <-- Operation proceeds without owner check
+    _LOGGER.info(f'User[U:{result.uid}] opened election[E:{election.eid}]')
+    # ...
+```
+
+**Also in `load_election` decorator:**
+
+```python
+def load_election(func):
+    @functools.wraps(func)
+    async def loader(eid):
+        # ...
+        _LOGGER.debug(f'Loaded: {e}')
+        ### check authz      # <-- AUTHORIZATION NOT IMPLEMENTED
+        return await func(e)
+    return loader
+```
+
+**Data Flow:**
+1. Source: Any authenticated committer
+2. Expected control: Verify requester is election owner (`owner_pid == result.uid`) before allowing management operations
+3. Missing: No ownership/role check, no authorization decision, no authorization logging
+4. Sink: Critical election state changes occur without authorization verification
+
+**Proof of Concept:**
+
+A committer who did NOT create an election sends `GET /do-close/TARGET_EID`. The election is closed. The log records `User[U:attacker] closed election[E:TARGET_EID]` as a successful action, but there is no log entry indicating this was an unauthorized action by a non-owner.
+
+**Impact:**
+
+Since authorization checks are absent, any authenticated committer can open, close, or modify any election. Because no authorization decision is made, there is nothing to log as a "failed authorization attempt." This creates a complete blind spot — unauthorized actions appear as normal authorized operations in logs, making security incident detection impossible.
+
+**Remediation:**
+
+Add authorization checks in `load_election` (for management endpoints) and log both successful and failed authorization decisions:
+
+```python
+def load_election_owner(func):
+    """Decorator to load Election and verify ownership."""
+    @functools.wraps(func)
+    async def loader(eid):
+        result = await basic_info()
+        try:
+            e = steve.election.Election(DB_FNAME, eid)
+        except steve.election.ElectionNotFound:
+            # ... existing 404 handling ...
+
+        md = e.get_metadata()
+        if md.owner_pid != result.uid:
+            _LOGGER.warning(
+                f'Authorization failure: User[U:{result.uid}] denied'
+                f' management of election[E:{eid}]'
+                f' owned by[U:{md.owner_pid}]'
+            )
+            quart.abort(403)
+
+        _LOGGER.info(
+            f'Authorization granted: User[U:{result.uid}]'
+            f' accessing management of election[E:{eid}]'
+        )
+        return await func(e)
+    return loader
+```
+
+---
+
+### Medium Severity
+
+#### ASVS-1632-MEDIUM-005
+
+**Title:** Tampering detection event logged via `print()` instead of logging framework
+
+**Location:**
+- **File:** `v3/server/bin/tally.py`
+- **Function:** `main`
+- **Lines:** ~145-147
+
+**Related Functions Checked:**
+- `main` (tampering detection logic)
+- `tally_election` (exception handling also uses `print()`)
+
+**Description:**
+
+The tally script detects election data tampering using `election.is_tampered(pdb)`, which is a critical security control. However, when tampering is detected, the event is logged using `print()` instead of the logging framework. This represents a Type B gap — the logging framework (`_LOGGER`) is defined and used elsewhere, but this critical security event uses `print()` instead.
+
+**Vulnerable Code:**
+
+```python
+if election.is_tampered(pdb):
+    print(f'Error: Election {election_id} has been tampered with. Cannot proceed.')
+    sys.exit(1)
+```
+
+**Additional instance in `tally_election`:**
+
+```python
+except Exception as e:
+    print(f'Error tallying issue {issue.iid}: {e}')
+    raise  # Fail hard
+```
+
+**Data Flow:**
+1. Source: Tamper detection via `election.is_tampered(pdb)`
+2. Security event: Election data integrity violation detected
+3. Missing control: `print()` goes to stdout which may not be captured by log aggregation/SIEM systems
+4. Note: `_LOGGER = logging.getLogger(__name__)` is defined at line ~29 but not used here
+
+**Impact:**
+
+Tampering detection is a critical security event that should be reliably captured in security logs. Using `print()` instead of the logging framework means this event:
+- May not be captured if stdout is not redirected to a log file
+- Won't have structured metadata (timestamp, level, module)
+- Won't be picked up by log aggregation or SIEM systems
+- Won't be preserved if the process terminates unexpectedly
+
+**Remediation:**
+
+```python
+if election.is_tampered(pdb):
+    _LOGGER.critical(
+        f'TAMPERING DETECTED: election[E:{election_id}]'
+        f' integrity check failed — tally aborted'
+    )
+    print(f'Error: Election {election_id} has been tampered with. Cannot proceed.')
+    sys.exit(1)
+```
+
+---
+
+## Positive Security Patterns
+
+1. **Structured action logging for mutations**: Successful operations in `pages.py` consistently use `_LOGGER.info()` with structured identifiers (`User[U:...]`, `election[E:...]`, `issue[I:...]`), providing good traceability for successful actions.
+
+2. **Vote content not logged**: `do_vote_endpoint` correctly logs `User[U:{result.uid}] voted on issue[I:{iid}]` without including the vote content — maintaining ballot secrecy per domain requirements.
+
+3. **Error logging on vote failures**: The vote submission error path logs the failure with user and issue context via `_LOGGER.error()`.
+
+4. **Election state change logging**: Opening and closing elections are logged with user identity, creating an audit trail for election lifecycle events.
+
+5. **Tamper detection before tally**: The tally script checks `election.is_tampered()` before proceeding, which is a strong integrity control even though the event is not properly logged.
+
+---
+
+## Architecture Observations
+
+1. **Logging framework is well-established but inconsistently applied**: The `_LOGGER` pattern is used throughout `pages.py` for successful operations but completely absent from authorization failure paths and the tally script's critical operations.
+
+2. **Authorization is split between framework and application**: The `asfquart.auth.require` decorator handles coarse-grained authentication/role checks, but fine-grained authorization (election ownership, voter eligibility) must be implemented at the application level — and is largely missing with `### check authz` TODO markers.
+
+3. **CLI vs. Web asymmetry in logging**: The web server (`pages.py`) has reasonable action logging, but the CLI tally script (`tally.py`) — which performs the most sensitive operations — has essentially no audit logging at all.
+
+4. **No centralized authorization decision logging**: There is no utility function or decorator for logging authorization decisions. Each endpoint would need to implement its own logging, increasing the risk of inconsistency.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions (Critical Priority)
+
+| Priority | Action | Findings Addressed |
+|----------|--------|-------------------|
+| **1** | Add authorization checks AND authorization decision logging to all election management endpoints (`do-open`, `do-close`, `do-add-issue`, `do-edit-issue`, `do-delete-issue`, `manage`, `do-set-*_at`) — implement an `authorize_owner` decorator that logs both grants and denials | ASVS-1632-HIGH-004 |
+| **2** | Add comprehensive audit logging to `tally.py` — log tally initiation, election/issue being tallied, spy-on-open usage, completion, and use `_LOGGER` instead of `print()` for all security events | ASVS-1632-HIGH-003, ASVS-1632-MEDIUM-005 |
+| **3** | Add authorization failure logging to `vote_on_page` and `serve_doc` where mayvote checks already exist | ASVS-1632-HIGH-001, ASVS-1632-HIGH-002 |
+
+### Short-term Actions
+
+- Create a centralized `log_authz_decision(uid, resource, action, granted, reason)` utility to ensure consistent structured logging of all authorization decisions across the application
+
+### Long-term Actions
+
+- For L3 compliance: Add logging of all authorization grants (not just failures) to all endpoints, including when sensitive election data is viewed through management pages
+- Implement automated monitoring and alerting on authorization failure patterns to detect reconnaissance or brute-force attempts
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list</summary>
+
+- `v3/server/bin/tally.py` (relevance: 10)
+- `v3/server/main.py` (relevance: 10)
+- `v3/server/pages.py` (relevance: 10)
+- `v3/steve/election.py` (relevance: 10)
+
+</details>
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org