[SECURITY] Production security logging with correlation IDs and audit trail - closes #55#84
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new security/audit logging module with correlation ID context support and accompanying unit tests.
Changes:
- New
pt_security_loggermodule providingSecurityLogger,SecurityEvent,SecurityEventType, and thread-local correlation ID utilities. - Dedicated rotating JSONL audit log with owner-only file permissions and a
CorrelationLogFilter. - Comprehensive unit tests covering correlation context, event serialization, and security event logging.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 16 comments.
| File | Description |
|---|---|
| app/pt_security_logger.py | New module implementing security audit logging, correlation IDs, and a module-level singleton. |
| app/test_security_logger.py | Tests for correlation IDs, SecurityEvent, and SecurityLogger methods. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Local CI Simulation ResultsRan all workflow steps locally against this branch prior to owner approval. Results: Code Quality & Testing
Unit Test Results
CI/CD Pipeline
|
…racking pt_security_logger.py: - SecurityLogger with RotatingFileHandler (10MB/10 backups), secure permissions, JSONL structured events - SecurityEventType enum: auth, credential, suspicious, trade, system events - correlation_context() thread-local context manager propagates request IDs - CorrelationLogFilter injects correlation_id into all LogRecords - Module-level security_logger singleton for application-wide use - 25 unit tests, all passing
0e1e135 to
4645e27
Compare
pt_security_logger.py: - Module-level singleton is now lazy (get_security_logger()) — audit file not created at import time; default log_dir is ~/.powertraderai/logs, not the source tree; os.makedirs ensures dir exists before handler creation - Per-instance logging.Logger (not global registry) eliminates the shared-handler/_handler=None pitfall when two instances share a log_dir - _SecureRotatingFileHandler subclass overrides doRollover() to chmod active file and all backups after rotation - _chmod_secure() called once at handler creation, not on every _emit(); OSError logged as WARNING instead of silently swallowed - correlation_context renamed to CorrelationContext (PEP 8 CapWords); snake_case alias kept for backwards compatibility - CorrelationContext uses a _stack list so nested/reused instances are safe (no _previous corruption) - clear_correlation_id() uses del instead of setting to None - SecurityEvent.new_id() uses datetime.now(timezone.utc) for UTC IDs - get_recent_events() uses collections.deque(f, maxlen=limit) for O(limit) tail read instead of readlines() O(file size) - hashlib.md5 removed (per-instance logger needs no path hash) - Added log_rate_limit() and log_config_change() helpers for enum values that previously had no public method - Generator import removed (was unused) - Docstring updated: module does NOT extend pt_logging_system test_security_logger.py: - tearDown uses sec_logger.close() to release file handles - test_get_recent_events_empty_when_no_log: adds assertEqual([], ...) assertion - test_get_recent_events_limit: asserts returned events are the last N - Added test_new_id_uses_utc, test_context_manager_nested_safe, test_snake_case_alias_works, test_log_rate_limit, test_log_config_change
Manual Testing Guide — PR #84: Production Security LoggingPrerequisitesgit fetch fork && git checkout feat/55-production-logging
cd app1. Run unit testspython -m pytest test_security_logger.py -vExpected: All 25+ tests pass. 2. Smoke test — no file created at import time (key fix)python -c "
import sys, os
# Record files in app/ before import
before = set(os.listdir('.'))
import pt_security_logger # module import must NOT create security_audit.jsonl
after = set(os.listdir('.'))
new_files = after - before
print('new files on import:', new_files)
assert 'security_audit.jsonl' not in new_files, 'audit file created at import!'
print('PASS: no file created at import')
"3. Smoke test — audit file goes to ~/.powertraderai/logs (key fix)python -c "
import os, tempfile
from pt_security_logger import SecurityLogger
# Explicit log_dir
with tempfile.TemporaryDirectory() as d:
sl = SecurityLogger(log_dir=d)
sl.log_auth_attempt('binance', success=True)
events = sl.get_recent_events()
print('events logged:', len(events))
assert len(events) == 1
print('event_type:', events[0]['event_type'])
sl.close()
print('PASS')
"4. Verify correlation ID context nesting (key fix)python -c "
from pt_security_logger import CorrelationContext, get_correlation_id, clear_correlation_id
import tempfile
clear_correlation_id()
ctx = CorrelationContext('outer')
with ctx:
print('outer:', get_correlation_id()) # outer
with ctx:
print('nested:', get_correlation_id()) # outer (same instance reused safely)
print('after inner exit:', get_correlation_id()) # outer
print('after outer exit:', get_correlation_id()) # None
print('PASS')
"5. Verify rate_limit and config_change helpers exist (key fix)python -c "
import tempfile
from pt_security_logger import SecurityLogger, SecurityEventType
with tempfile.TemporaryDirectory() as d:
sl = SecurityLogger(log_dir=d)
sl.log_rate_limit('robinhood', details={'endpoint': '/orders'})
sl.log_config_change('risk_limits', details={'max_pos': 0.05})
events = sl.get_recent_events()
types = [e['event_type'] for e in events]
print('event types:', types)
assert SecurityEventType.RATE_LIMIT.value in types
assert SecurityEventType.CONFIG_CHANGE.value in types
sl.close()
print('PASS')
"6. Verify audit file permissions (Unix only)python -c "
import tempfile, stat, os
from pt_security_logger import SecurityLogger
with tempfile.TemporaryDirectory() as d:
sl = SecurityLogger(log_dir=d)
sl.log_auth_attempt('test', success=True)
mode = oct(stat.S_IMODE(os.stat(sl._audit_path).st_mode))
print('permissions:', mode) # should be 0o600
sl.close()
"Expected on Unix: Rollbackgit checkout main -- app/pt_security_logger.py app/test_security_logger.py |
- Remove unused hashlib import - close(): make idempotent by clearing self._handler after shutdown - get_recent_events: guard limit <= 0 (deque maxlen rejects negatives); docstring corrected to reflect O(file_size) time / O(limit) memory - test_multiple_events_ordered: assert event_type append order (robust to clock adjustments) + non-decreasing timestamp sanity check
|
All 23 outstanding Copilot threads resolved. Round-1/2 threads (already replied inline) marked resolved; round-3 (5 new) addressed in commit
All 30 |
|
@sjackson0109 all 23 outstanding Copilot threads cleared (5 round-3 fixed in 08dd045, 18 round-1/2 resolved). 30 tests pass. Ready for re-review. |
637b547 to
98cfa3c
Compare
…gging # Conflicts: # app/pt_security_logger.py
Copilot review follow-up (commit 5f96c43)Merged
Black + flake8 clean, 30 unit tests pass. |
Signed-off-by: Simon Jackson <simon@jacksonfamily.me>
|
@sjackson0109 I have updated the PR description to correctly state that pt_security_logger is a standalone module and does not depend on pt_logging_system. I also updated the usage examples in the description to use CorrelationContext for consistency with the code. This addresses the documentation mismatch flagged by Copilot. |
|
@copilot do final review |
Summary
Creates pt_security_logger.py as a standalone security event logging module (independent of pt_logging_system.py) providing security-specific event logging, correlation ID propagation, and a dedicated rotating audit log.
Changes
Key features
Usage
\\python
from pt_security_logger import security_logger, CorrelationContext
Propagate correlation ID through a workflow
with CorrelationContext('order-workflow-abc') as cid:
security_logger.log_auth_attempt('robinhood', success=True)
security_logger.log_trade_event('BTC-USD', 'buy', 0.1, 45000.0)
Suspicious activity alert
security_logger.log_suspicious_activity(
'rate_limit_exceeded', source_ip='1.2.3.4',
details={'endpoint': '/orders', 'count': 150}
)
\\
Test plan
Closes #55