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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ app/AVAX/
.DS_Store
Thumbs.db
desktop.ini
app/security_audit.jsonl
app/credential_audit.jsonl

# Contributor working notes (not project docs)
CLAUDE.md
LEARNINGS.md

# Local-only skills (not committed to repo)
.github/skills/
Expand Down
42 changes: 29 additions & 13 deletions app/pt_security_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
# Security event types
# ---------------------------------------------------------------------------
class SecurityEventType(Enum):
AUTH_ATTEMPT = "auth_attempt" # API key authentication attempt
AUTH_SUCCESS = "auth_success" # Successful authentication
AUTH_FAILURE = "auth_failure" # Failed authentication
CREDENTIAL_USE = "credential_use" # Credential accessed/used
Comment thread
sjackson0109 marked this conversation as resolved.
Expand Down Expand Up @@ -248,16 +247,20 @@ def _create_handler(self) -> _SecureRotatingFileHandler:

def close(self) -> None:
"""Flush and close the underlying handler (call on shutdown).
Idempotent: safe to call multiple times."""
if self._handler is None:
return
handler = self._handler
self._handler = None
try:
handler.flush()
handler.close()
finally:
self._audit_logger.removeHandler(handler)
Idempotent: safe to call multiple times.

Acquires the same lock as ``_emit`` so a concurrent emit cannot
write to a handler that is mid-teardown."""
with self._lock:
if self._handler is None:
return
handler = self._handler
self._handler = None
try:
handler.flush()
handler.close()
finally:
self._audit_logger.removeHandler(handler)
Comment thread
sjackson0109 marked this conversation as resolved.

def _emit(self, event: SecurityEvent) -> None:
"""Write security event to audit log (thread-safe)."""
Expand Down Expand Up @@ -477,7 +480,11 @@ def log_system_event(
message: str,
details: Optional[Dict[str, Any]] = None,
) -> None:
"""Log a system lifecycle event (start, stop, config change, rate limit)."""
"""Log a system event under the given ``event_type``.

Accepts any ``SecurityEventType`` (start/stop, config change, rate
limit, permission denied, etc.); the listed examples are illustrative,
not exhaustive."""
Comment thread
Ibrahim-3d marked this conversation as resolved.
self._emit(self._make_event(event_type, message, details=details))
Comment thread
Ibrahim-3d marked this conversation as resolved.

def get_recent_events(self, limit: int = 100) -> List[dict]:
Expand All @@ -499,13 +506,22 @@ def get_recent_events(self, limit: int = 100) -> List[dict]:
with open(self._audit_path, "r", encoding="utf-8") as f:
tail = collections.deque(f, maxlen=limit)
Comment thread
Ibrahim-3d marked this conversation as resolved.
events = []
corrupt = 0
for line in tail:
line = line.strip()
if line:
try:
events.append(json.loads(line))
except json.JSONDecodeError:
pass
# Surface, don't hide: a malformed line in an audit
# trail may indicate truncation or tampering.
corrupt += 1
if corrupt:
logger.warning(
"Skipped %d unparseable line(s) in audit log %s",
corrupt,
self._audit_path,
)
return events
Comment thread
Ibrahim-3d marked this conversation as resolved.
except OSError:
return []
Expand Down
2 changes: 1 addition & 1 deletion app/test_security_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def test_new_id_uses_utc(self):
def test_to_dict_has_required_fields(self):
event = SecurityEvent(
event_id="SEC_001",
event_type=SecurityEventType.AUTH_ATTEMPT.value,
event_type=SecurityEventType.AUTH_SUCCESS.value,
timestamp=time.time(),
message="test",
correlation_id=None,
Expand Down
Loading