20f20fa321
* fix(security): invalidate profile cache when file is created/modified Fixes #153 The security profile cache was returning stale data even after the .auto-claude-security.json file was created or updated. This caused commands like 'dotnet' to be blocked even when present in the file. Root cause: get_security_profile() cached the profile on first call without checking if the file's mtime changed on subsequent calls. Fix: Track the security profile file's mtime and invalidate the cache when the file is created (mtime goes from None to a value) or modified (mtime changes). This also helps with issue #222 where the profile is created after the agent starts - now the agent will pick up the new profile on the next command validation. Signed-off-by: Black Circle Sentinel <mludlow000@icloud.com> * fix(security): handle deleted profile file and add cache invalidation tests * fix(security): handle deleted profile file and add cache invalidation tests * test(security): improve cache tests with mocks and unique commands * test(security): add mock-free tests for cache invalidation * test(security): fix cache invalidation tests without mocks * fix(security): address review comments and add debug logs for CI hash failure * fix(analyzer): remove debug prints * fix(lint): sort imports in profile.py * fix(security): include spec_dir in cache key to prevent stale profiles The cache key previously only included project_dir, but the profile location can depend on spec_dir. This could cause stale cached profiles to be returned if spec_dir changes between calls. Fix: Add _cached_spec_dir to the cache validation logic and reset function. --------- Signed-off-by: Black Circle Sentinel <mludlow000@icloud.com> Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
"""
|
|
Security Profile Management
|
|
============================
|
|
|
|
Manages security profiles for projects, including caching and validation.
|
|
Uses project_analyzer to create dynamic security profiles based on detected stacks.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from project_analyzer import (
|
|
ProjectAnalyzer,
|
|
SecurityProfile,
|
|
get_or_create_profile,
|
|
)
|
|
|
|
# =============================================================================
|
|
# GLOBAL STATE
|
|
# =============================================================================
|
|
|
|
# Cache the security profile to avoid re-analyzing on every command
|
|
_cached_profile: SecurityProfile | None = None
|
|
_cached_project_dir: Path | None = None
|
|
_cached_spec_dir: Path | None = None # Track spec directory for cache key
|
|
_cached_profile_mtime: float | None = None # Track file modification time
|
|
|
|
|
|
def _get_profile_path(project_dir: Path) -> Path:
|
|
"""Get the security profile file path for a project."""
|
|
return project_dir / ProjectAnalyzer.PROFILE_FILENAME
|
|
|
|
|
|
def _get_profile_mtime(project_dir: Path) -> float | None:
|
|
"""Get the modification time of the security profile file, or None if not exists."""
|
|
profile_path = _get_profile_path(project_dir)
|
|
try:
|
|
return profile_path.stat().st_mtime if profile_path.exists() else None
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def get_security_profile(
|
|
project_dir: Path, spec_dir: Path | None = None
|
|
) -> SecurityProfile:
|
|
"""
|
|
Get the security profile for a project, using cache when possible.
|
|
|
|
The cache is invalidated when:
|
|
- The project directory changes
|
|
- The security profile file is created (was None, now exists)
|
|
- The security profile file is modified (mtime changed)
|
|
|
|
Args:
|
|
project_dir: Project root directory
|
|
spec_dir: Optional spec directory
|
|
|
|
Returns:
|
|
SecurityProfile for the project
|
|
"""
|
|
global _cached_profile, _cached_project_dir, _cached_spec_dir, _cached_profile_mtime
|
|
|
|
project_dir = Path(project_dir).resolve()
|
|
resolved_spec_dir = Path(spec_dir).resolve() if spec_dir else None
|
|
|
|
# Check if cache is valid (both project_dir and spec_dir must match)
|
|
if (
|
|
_cached_profile is not None
|
|
and _cached_project_dir == project_dir
|
|
and _cached_spec_dir == resolved_spec_dir
|
|
):
|
|
# Check if file has been created or modified since caching
|
|
current_mtime = _get_profile_mtime(project_dir)
|
|
# Cache is valid if:
|
|
# - Both are None (file never existed and still doesn't)
|
|
# - Both have same mtime (file unchanged)
|
|
if current_mtime == _cached_profile_mtime:
|
|
return _cached_profile
|
|
|
|
# File was created or modified - invalidate cache
|
|
# (This happens when analyzer creates the file after agent starts)
|
|
|
|
# Analyze and cache
|
|
_cached_profile = get_or_create_profile(project_dir, spec_dir)
|
|
_cached_project_dir = project_dir
|
|
_cached_spec_dir = resolved_spec_dir
|
|
_cached_profile_mtime = _get_profile_mtime(project_dir)
|
|
|
|
return _cached_profile
|
|
|
|
|
|
def reset_profile_cache() -> None:
|
|
"""Reset the cached profile (useful for testing or re-analysis)."""
|
|
global _cached_profile, _cached_project_dir, _cached_spec_dir, _cached_profile_mtime
|
|
_cached_profile = None
|
|
_cached_project_dir = None
|
|
_cached_spec_dir = None
|
|
_cached_profile_mtime = None
|