securityagent-core 4.18.0__tar.gz → 4.24.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. {securityagent_core-4.18.0/src/securityagent_core.egg-info → securityagent_core-4.24.0}/PKG-INFO +4 -2
  2. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/pyproject.toml +3 -2
  3. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/cli.py +28 -0
  4. securityagent_core-4.24.0/src/endpoint_agent/scanners/active_learner.py +251 -0
  5. {securityagent_core-4.18.0 → securityagent_core-4.24.0/src/securityagent_core.egg-info}/PKG-INFO +4 -2
  6. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/securityagent_core.egg-info/SOURCES.txt +1 -0
  7. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/securityagent_core.egg-info/requires.txt +4 -1
  8. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/compliance_report.py +177 -2
  9. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/LICENSE +0 -0
  10. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/MANIFEST.in +0 -0
  11. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/README.md +0 -0
  12. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/setup.cfg +0 -0
  13. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/__init__.py +0 -0
  14. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/__main__.py +0 -0
  15. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/alerting/__init__.py +0 -0
  16. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/alerting/alert_manager.py +0 -0
  17. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/alerting/console_handler.py +0 -0
  18. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/alerting/log_handler.py +0 -0
  19. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/alerting/webhook_handler.py +0 -0
  20. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/cloud_bridge/__init__.py +0 -0
  21. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/cloud_bridge/lockdown.py +0 -0
  22. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/config/__init__.py +0 -0
  23. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/config/privacy_mode.py +0 -0
  24. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/config/settings.py +0 -0
  25. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/engine.py +0 -0
  26. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/monitors/__init__.py +0 -0
  27. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/monitors/behavioral_monitor.py +0 -0
  28. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/monitors/file_monitor.py +0 -0
  29. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/monitors/honeypot_monitor.py +0 -0
  30. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/monitors/privilege_monitor.py +0 -0
  31. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/monitors/process_monitor.py +0 -0
  32. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/__init__.py +0 -0
  33. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/code_fingerprint.py +0 -0
  34. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/content_analyzer.py +0 -0
  35. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/credential_scanner.py +0 -0
  36. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/data_flow_tracker.py +0 -0
  37. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/dlp_scanner.py +0 -0
  38. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/finding_validators.py +0 -0
  39. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/installation_scanner.py +0 -0
  40. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/knowledge_graph.py +0 -0
  41. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/lethal_trifecta.py +0 -0
  42. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/llm_analyzer.py +0 -0
  43. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/llm_provider.py +0 -0
  44. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/memory_guard.py +0 -0
  45. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/ml_classifier.py +0 -0
  46. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/model_locality.py +0 -0
  47. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/nist_tagger.py +0 -0
  48. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/prompt_analyzer.py +0 -0
  49. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/prompt_guard.py +0 -0
  50. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/rule_classifier.py +0 -0
  51. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/shadow_ai_detector.py +0 -0
  52. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/text_extractor.py +0 -0
  53. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/tool_call_guard.py +0 -0
  54. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/scanners/vuln_scanner.py +0 -0
  55. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/secure_fs.py +0 -0
  56. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/__init__.py +0 -0
  57. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_alert_manager.py +0 -0
  58. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_behavioral_monitor.py +0 -0
  59. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_cloud_bridge.py +0 -0
  60. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_credential_scanner.py +0 -0
  61. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_data_flow_tracker.py +0 -0
  62. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_dlp_scanner.py +0 -0
  63. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_engine.py +0 -0
  64. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_file_monitor.py +0 -0
  65. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_honeypot_monitor.py +0 -0
  66. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_installation_scanner.py +0 -0
  67. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_llm_analyzer.py +0 -0
  68. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_privilege_monitor.py +0 -0
  69. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_process_monitor.py +0 -0
  70. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_prompt_analyzer.py +0 -0
  71. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_prompt_guard.py +0 -0
  72. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_rule_classifier.py +0 -0
  73. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_secure_fs.py +0 -0
  74. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_semantic_disclosure.py +0 -0
  75. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_text_extractor.py +0 -0
  76. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_tool_call_guard.py +0 -0
  77. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/tests/test_trial_manager.py +0 -0
  78. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/endpoint_agent/trial_manager.py +0 -0
  79. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/plugin.py +0 -0
  80. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/policy/__init__.py +0 -0
  81. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/policy/audit.py +0 -0
  82. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/policy/chain_detector.py +0 -0
  83. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/policy/engine.py +0 -0
  84. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/policy/memory_bridge.py +0 -0
  85. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/policy/rules.py +0 -0
  86. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/policy/session.py +0 -0
  87. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/scripts/secagent_check.py +0 -0
  88. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/securityagent_core/__init__.py +0 -0
  89. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/securityagent_core.egg-info/dependency_links.txt +0 -0
  90. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/securityagent_core.egg-info/entry_points.txt +0 -0
  91. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/securityagent_core.egg-info/top_level.txt +0 -0
  92. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/__init__.py +0 -0
  93. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/adapters/__init__.py +0 -0
  94. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/adapters/cli_adapter.py +0 -0
  95. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/adapters/mcp_server.py +0 -0
  96. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/adapters/mcp_sse_server.py +0 -0
  97. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/adapters/python_sdk.py +0 -0
  98. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/base.py +0 -0
  99. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/context.py +0 -0
  100. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/__init__.py +0 -0
  101. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/analyze_prompt.py +0 -0
  102. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/audit_log.py +0 -0
  103. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/check_policy.py +0 -0
  104. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/get_session_policy.py +0 -0
  105. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/scan_output.py +0 -0
  106. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/secure_exec.py +0 -0
  107. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/secure_read.py +0 -0
  108. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/implementations/token_optimize.py +0 -0
  109. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/src/skills/registry.py +0 -0
  110. {securityagent_core-4.18.0 → securityagent_core-4.24.0}/tests/test_prompt_scenarios.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: securityagent-core
3
- Version: 4.18.0
3
+ Version: 4.24.0
4
4
  Summary: Local-first DLP engine and security layer for AI coding agents. Secures Copilot, Claude Code, Cursor, Windsurf, and any MCP-compatible agent.
5
5
  Author-email: SecureMind <founders@securemind.live>
6
6
  License: Proprietary
@@ -41,11 +41,13 @@ Provides-Extra: ocr
41
41
  Requires-Dist: PyMuPDF; extra == "ocr"
42
42
  Requires-Dist: pytesseract; extra == "ocr"
43
43
  Requires-Dist: Pillow; extra == "ocr"
44
+ Provides-Extra: pdf
45
+ Requires-Dist: fpdf2; extra == "pdf"
44
46
  Provides-Extra: dev
45
47
  Requires-Dist: pytest; extra == "dev"
46
48
  Requires-Dist: moto; extra == "dev"
47
49
  Provides-Extra: all
48
- Requires-Dist: securityagent-core[cloud,llm,ml,ocr]; extra == "all"
50
+ Requires-Dist: securityagent-core[cloud,llm,ml,ocr,pdf]; extra == "all"
49
51
  Dynamic: license-file
50
52
 
51
53
  # securityagent-core
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "securityagent-core"
7
- version = "4.18.0"
7
+ version = "4.24.0"
8
8
  description = "Local-first DLP engine and security layer for AI coding agents. Secures Copilot, Claude Code, Cursor, Windsurf, and any MCP-compatible agent."
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -46,8 +46,9 @@ ml = ["sentence-transformers", "scikit-learn", "joblib", "numpy"]
46
46
  cloud = ["boto3", "PyJWT", "cryptography"]
47
47
  llm = ["ollama"]
48
48
  ocr = ["PyMuPDF", "pytesseract", "Pillow"]
49
+ pdf = ["fpdf2"]
49
50
  dev = ["pytest", "moto"]
50
- all = ["securityagent-core[ml,cloud,llm,ocr]"]
51
+ all = ["securityagent-core[ml,cloud,llm,ocr,pdf]"]
51
52
 
52
53
  [tool.setuptools.packages.find]
53
54
  where = ["src"]
@@ -294,6 +294,33 @@ def cmd_demo(args):
294
294
 
295
295
  def cmd_train(args):
296
296
  """Train/retrain the ML classifier."""
297
+ if args.auto:
298
+ # Active learning: check if retraining is needed
299
+ print(BANNER)
300
+ print(" \033[1mActive Learning Check\033[0m\n")
301
+ from endpoint_agent.scanners.active_learner import ActiveLearner
302
+ learner = ActiveLearner()
303
+ check = learner.should_retrain()
304
+ print(f" New audit entries: {check['new_entries']}")
305
+ print(f" Total entries: {check['total_entries']}")
306
+ print(f" Threshold: {learner.retrain_threshold}")
307
+ print(f" Decision: {'RETRAIN' if check['should_retrain'] else 'SKIP'}")
308
+ print(f" Reason: {check['reason']}\n")
309
+
310
+ if check["should_retrain"]:
311
+ result = learner.check_and_retrain()
312
+ if result["action"] == "retrained":
313
+ print(f" \033[92mRetrained successfully!\033[0m")
314
+ print(f" Intent F1: {result['intent_f1']['old']:.3f} -> {result['intent_f1']['new']:.3f}")
315
+ print(f" PII F1: {result['pii_f1']['old']:.3f} -> {result['pii_f1']['new']:.3f}")
316
+ elif result["action"] == "rejected":
317
+ print(f" \033[93mNew model rejected:\033[0m {result['reason']}")
318
+ else:
319
+ print(f" \033[90m{result.get('reason', 'Unknown')}\033[0m")
320
+ else:
321
+ print(f" \033[90mNo retraining needed.\033[0m")
322
+ return
323
+
297
324
  train_script = Path.home() / "AgnosticSecurity" / "scripts" / "train_enhanced_classifier.py"
298
325
 
299
326
  if not train_script.exists():
@@ -429,6 +456,7 @@ Examples:
429
456
  # train
430
457
  train_p = subparsers.add_parser("train", help="Train/retrain ML classifier")
431
458
  train_p.add_argument("--eval", action="store_true", help="Run eval after training")
459
+ train_p.add_argument("--auto", action="store_true", help="Active learning: auto-retrain if enough new data")
432
460
 
433
461
  # mcp
434
462
  mcp_p = subparsers.add_parser("mcp", help="Start MCP security server")
@@ -0,0 +1,251 @@
1
+ """Active learning pipeline — auto-retrain ML classifiers from new audit data.
2
+
3
+ Monitors hook_audit.jsonl for new entries since last training, retrains
4
+ when sufficient new data accumulates, and validates before promoting.
5
+
6
+ Usage:
7
+ from endpoint_agent.scanners.active_learner import ActiveLearner
8
+ learner = ActiveLearner()
9
+ learner.check_and_retrain() # Called periodically or via secagent train --auto
10
+
11
+ Flow:
12
+ 1. Load last training timestamp from model metadata
13
+ 2. Count new audit entries since then
14
+ 3. If >= threshold (default 200), trigger retrain
15
+ 4. Train on full dataset (old + new)
16
+ 5. Validate: new model must beat old model's CV F1
17
+ 6. If passes, promote; if not, keep old model
18
+ """
19
+
20
+ import json
21
+ import logging
22
+ import os
23
+ import time
24
+ from pathlib import Path
25
+ from typing import Dict, Optional
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ _MODEL_DIR = os.environ.get(
30
+ "EA_ML_MODEL_DIR",
31
+ os.path.join(os.path.expanduser("~"), ".agnosticsecurity", "models"),
32
+ )
33
+ _METADATA_PATH = os.path.join(_MODEL_DIR, "training_metadata.json")
34
+ _INTENT_MODEL_PATH = os.path.join(_MODEL_DIR, "intent_classifier_v2.joblib")
35
+ _PII_MODEL_PATH = os.path.join(_MODEL_DIR, "pii_context_classifier_v2.joblib")
36
+
37
+ # Default audit log locations
38
+ _AUDIT_PATHS = [
39
+ os.path.join(os.path.expanduser("~"), "AgnosticSecurity", "logs", "hook_audit.jsonl"),
40
+ os.path.join(os.path.expanduser("~"), ".securityagent", "skills_audit.jsonl"),
41
+ ]
42
+
43
+ # Minimum new entries before retraining
44
+ _RETRAIN_THRESHOLD = int(os.environ.get("EA_RETRAIN_THRESHOLD", "200"))
45
+ # Minimum F1 improvement required to promote new model
46
+ _MIN_F1_IMPROVEMENT = float(os.environ.get("EA_MIN_F1_IMPROVEMENT", "-0.02"))
47
+
48
+
49
+ def _load_metadata() -> Dict:
50
+ """Load training metadata (last train time, metrics)."""
51
+ if os.path.exists(_METADATA_PATH):
52
+ try:
53
+ with open(_METADATA_PATH) as f:
54
+ return json.load(f)
55
+ except (json.JSONDecodeError, IOError):
56
+ pass
57
+ return {"last_trained_at": 0, "last_entry_count": 0, "intent_f1": 0.0, "pii_f1": 0.0}
58
+
59
+
60
+ def _save_metadata(metadata: Dict):
61
+ """Save training metadata."""
62
+ os.makedirs(os.path.dirname(_METADATA_PATH), exist_ok=True)
63
+ with open(_METADATA_PATH, "w") as f:
64
+ json.dump(metadata, f, indent=2)
65
+
66
+
67
+ def _count_entries_since(paths: list, since_ts: float) -> int:
68
+ """Count audit log entries newer than since_ts."""
69
+ count = 0
70
+ for path in paths:
71
+ if not os.path.exists(path):
72
+ continue
73
+ with open(path) as f:
74
+ for line in f:
75
+ try:
76
+ entry = json.loads(line)
77
+ ts = entry.get("ts", "")
78
+ # Parse ISO timestamp to epoch
79
+ if isinstance(ts, str) and ts:
80
+ import datetime
81
+ try:
82
+ dt = datetime.datetime.fromisoformat(ts)
83
+ if dt.timestamp() > since_ts:
84
+ count += 1
85
+ except (ValueError, TypeError):
86
+ count += 1 # Can't parse = assume new
87
+ elif isinstance(ts, (int, float)) and ts > since_ts:
88
+ count += 1
89
+ except (json.JSONDecodeError, TypeError):
90
+ continue
91
+ return count
92
+
93
+
94
+ def _total_entries(paths: list) -> int:
95
+ """Count total audit entries across all paths."""
96
+ count = 0
97
+ for path in paths:
98
+ if not os.path.exists(path):
99
+ continue
100
+ with open(path) as f:
101
+ for _ in f:
102
+ count += 1
103
+ return count
104
+
105
+
106
+ class ActiveLearner:
107
+ """Auto-retrain ML classifiers when new audit data accumulates."""
108
+
109
+ def __init__(
110
+ self,
111
+ audit_paths: Optional[list] = None,
112
+ retrain_threshold: int = _RETRAIN_THRESHOLD,
113
+ min_f1_improvement: float = _MIN_F1_IMPROVEMENT,
114
+ ):
115
+ self.audit_paths = audit_paths or _AUDIT_PATHS
116
+ self.retrain_threshold = retrain_threshold
117
+ self.min_f1_improvement = min_f1_improvement
118
+
119
+ def should_retrain(self) -> Dict:
120
+ """Check if retraining is needed.
121
+
122
+ Returns:
123
+ {"should_retrain": bool, "new_entries": int, "total_entries": int,
124
+ "reason": str}
125
+ """
126
+ metadata = _load_metadata()
127
+ last_trained = metadata.get("last_trained_at", 0)
128
+ new_entries = _count_entries_since(self.audit_paths, last_trained)
129
+ total = _total_entries(self.audit_paths)
130
+
131
+ if new_entries >= self.retrain_threshold:
132
+ return {
133
+ "should_retrain": True,
134
+ "new_entries": new_entries,
135
+ "total_entries": total,
136
+ "reason": f"{new_entries} new entries >= threshold {self.retrain_threshold}",
137
+ }
138
+
139
+ return {
140
+ "should_retrain": False,
141
+ "new_entries": new_entries,
142
+ "total_entries": total,
143
+ "reason": f"{new_entries} new entries < threshold {self.retrain_threshold}",
144
+ }
145
+
146
+ def check_and_retrain(self) -> Dict:
147
+ """Check if retraining is needed and do it if so.
148
+
149
+ Returns training results or skip reason.
150
+ """
151
+ check = self.should_retrain()
152
+ if not check["should_retrain"]:
153
+ logger.info("Active learner: skip — %s", check["reason"])
154
+ return {"action": "skipped", **check}
155
+
156
+ logger.info("Active learner: retraining — %s", check["reason"])
157
+ return self._retrain(check)
158
+
159
+ def _retrain(self, check: Dict) -> Dict:
160
+ """Execute retraining with validation gate."""
161
+ metadata = _load_metadata()
162
+ old_intent_f1 = metadata.get("intent_f1", 0.0)
163
+ old_pii_f1 = metadata.get("pii_f1", 0.0)
164
+
165
+ # Import training functions
166
+ try:
167
+ import sys
168
+ # Try AgnosticSecurity training script
169
+ train_script = os.path.join(
170
+ os.path.expanduser("~"), "AgnosticSecurity", "scripts"
171
+ )
172
+ if train_script not in sys.path:
173
+ sys.path.insert(0, train_script)
174
+
175
+ from train_enhanced_classifier import (
176
+ load_audit_log,
177
+ load_red_team_evals,
178
+ generate_synthetic_benign,
179
+ generate_pii_training_data,
180
+ train_intent_model,
181
+ train_pii_model,
182
+ AUDIT_LOG,
183
+ EVALS_DIR,
184
+ )
185
+ except ImportError as e:
186
+ return {
187
+ "action": "error",
188
+ "reason": f"Training script not found: {e}",
189
+ **check,
190
+ }
191
+
192
+ # Load data
193
+ audit_samples = load_audit_log(AUDIT_LOG)
194
+ red_team_samples = load_red_team_evals(EVALS_DIR)
195
+ synthetic = generate_synthetic_benign()
196
+ pii_samples = generate_pii_training_data()
197
+
198
+ intent_data = audit_samples + red_team_samples + synthetic
199
+ if len(intent_data) < 100:
200
+ return {
201
+ "action": "skipped",
202
+ "reason": f"Not enough training data ({len(intent_data)} samples)",
203
+ **check,
204
+ }
205
+
206
+ # Train new models
207
+ intent_result = train_intent_model(intent_data)
208
+ pii_result = train_pii_model(pii_samples)
209
+
210
+ new_intent_f1 = intent_result.get("cv_f1_macro", 0.0)
211
+ new_pii_f1 = pii_result.get("cv_f1_macro", 0.0)
212
+
213
+ # Validation gate: new model must not be significantly worse
214
+ intent_ok = new_intent_f1 >= (old_intent_f1 + self.min_f1_improvement)
215
+ pii_ok = new_pii_f1 >= (old_pii_f1 + self.min_f1_improvement)
216
+
217
+ if intent_ok and pii_ok:
218
+ # Promote: save metadata
219
+ _save_metadata({
220
+ "last_trained_at": time.time(),
221
+ "last_entry_count": check["total_entries"],
222
+ "intent_f1": new_intent_f1,
223
+ "pii_f1": new_pii_f1,
224
+ "intent_samples": intent_result.get("samples", 0),
225
+ "pii_samples": pii_result.get("samples", 0),
226
+ "trained_by": "active_learner",
227
+ })
228
+
229
+ return {
230
+ "action": "retrained",
231
+ "intent_f1": {"old": old_intent_f1, "new": new_intent_f1},
232
+ "pii_f1": {"old": old_pii_f1, "new": new_pii_f1},
233
+ "intent_samples": intent_result.get("samples", 0),
234
+ "pii_samples": pii_result.get("samples", 0),
235
+ **check,
236
+ }
237
+ else:
238
+ # Reject: new model is worse
239
+ reasons = []
240
+ if not intent_ok:
241
+ reasons.append(f"intent F1 dropped: {old_intent_f1:.3f} -> {new_intent_f1:.3f}")
242
+ if not pii_ok:
243
+ reasons.append(f"PII F1 dropped: {old_pii_f1:.3f} -> {new_pii_f1:.3f}")
244
+
245
+ return {
246
+ "action": "rejected",
247
+ "reason": "; ".join(reasons),
248
+ "intent_f1": {"old": old_intent_f1, "new": new_intent_f1},
249
+ "pii_f1": {"old": old_pii_f1, "new": new_pii_f1},
250
+ **check,
251
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: securityagent-core
3
- Version: 4.18.0
3
+ Version: 4.24.0
4
4
  Summary: Local-first DLP engine and security layer for AI coding agents. Secures Copilot, Claude Code, Cursor, Windsurf, and any MCP-compatible agent.
5
5
  Author-email: SecureMind <founders@securemind.live>
6
6
  License: Proprietary
@@ -41,11 +41,13 @@ Provides-Extra: ocr
41
41
  Requires-Dist: PyMuPDF; extra == "ocr"
42
42
  Requires-Dist: pytesseract; extra == "ocr"
43
43
  Requires-Dist: Pillow; extra == "ocr"
44
+ Provides-Extra: pdf
45
+ Requires-Dist: fpdf2; extra == "pdf"
44
46
  Provides-Extra: dev
45
47
  Requires-Dist: pytest; extra == "dev"
46
48
  Requires-Dist: moto; extra == "dev"
47
49
  Provides-Extra: all
48
- Requires-Dist: securityagent-core[cloud,llm,ml,ocr]; extra == "all"
50
+ Requires-Dist: securityagent-core[cloud,llm,ml,ocr,pdf]; extra == "all"
49
51
  Dynamic: license-file
50
52
 
51
53
  # securityagent-core
@@ -26,6 +26,7 @@ src/endpoint_agent/monitors/honeypot_monitor.py
26
26
  src/endpoint_agent/monitors/privilege_monitor.py
27
27
  src/endpoint_agent/monitors/process_monitor.py
28
28
  src/endpoint_agent/scanners/__init__.py
29
+ src/endpoint_agent/scanners/active_learner.py
29
30
  src/endpoint_agent/scanners/code_fingerprint.py
30
31
  src/endpoint_agent/scanners/content_analyzer.py
31
32
  src/endpoint_agent/scanners/credential_scanner.py
@@ -7,7 +7,7 @@ python-docx
7
7
  pyyaml
8
8
 
9
9
  [all]
10
- securityagent-core[cloud,llm,ml,ocr]
10
+ securityagent-core[cloud,llm,ml,ocr,pdf]
11
11
 
12
12
  [cloud]
13
13
  boto3
@@ -31,3 +31,6 @@ numpy
31
31
  PyMuPDF
32
32
  pytesseract
33
33
  Pillow
34
+
35
+ [pdf]
36
+ fpdf2
@@ -199,10 +199,15 @@ class ComplianceReportSkill(BaseSkill):
199
199
  },
200
200
  "format": {
201
201
  "type": "string",
202
- "enum": ["json", "markdown"],
203
- "description": "Output format",
202
+ "enum": ["json", "markdown", "pdf"],
203
+ "description": "Output format (pdf saves to ~/.securityagent/reports/)",
204
204
  "default": "json",
205
205
  },
206
+ "company_name": {
207
+ "type": "string",
208
+ "description": "Company name for PDF report header",
209
+ "default": "SecureMind",
210
+ },
206
211
  },
207
212
  }
208
213
 
@@ -258,6 +263,10 @@ class ComplianceReportSkill(BaseSkill):
258
263
 
259
264
  if fmt == "markdown":
260
265
  result_data["markdown"] = self._to_markdown(reports, stats)
266
+ elif fmt == "pdf":
267
+ company = params.get("company_name", "SecureMind")
268
+ pdf_path = self._to_pdf(reports, stats, company)
269
+ result_data["pdf_path"] = pdf_path
261
270
 
262
271
  return SkillResult(
263
272
  status=SkillStatus.ALLOWED,
@@ -294,3 +303,169 @@ class ComplianceReportSkill(BaseSkill):
294
303
  lines.append("")
295
304
 
296
305
  return "\n".join(lines)
306
+
307
+ def _to_pdf(self, reports: Dict, stats: Dict, company: str) -> str:
308
+ """Generate PDF compliance report.
309
+
310
+ Uses fpdf2 if available, otherwise falls back to a plain-text PDF
311
+ built with minimal PDF spec (no external deps).
312
+ """
313
+ report_dir = os.path.join(os.path.expanduser("~"), ".securityagent", "reports")
314
+ os.makedirs(report_dir, exist_ok=True)
315
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
316
+ pdf_path = os.path.join(report_dir, f"compliance_report_{timestamp}.pdf")
317
+
318
+ try:
319
+ from fpdf import FPDF
320
+ return self._pdf_with_fpdf(reports, stats, company, pdf_path)
321
+ except ImportError:
322
+ return self._pdf_fallback(reports, stats, company, pdf_path)
323
+
324
+ @staticmethod
325
+ def _sanitize(text: str) -> str:
326
+ """Replace unicode chars that Helvetica/latin-1 can't render."""
327
+ return text.encode("ascii", errors="replace").decode("ascii")
328
+
329
+ def _pdf_with_fpdf(self, reports: Dict, stats: Dict, company: str, path: str) -> str:
330
+ """Generate styled PDF using fpdf2."""
331
+ from fpdf import FPDF
332
+
333
+ pdf = FPDF()
334
+ pdf.set_auto_page_break(auto=True, margin=15)
335
+
336
+ # Title page
337
+ pdf.add_page()
338
+ pdf.set_font("Helvetica", "B", 24)
339
+ pdf.cell(0, 20, f"{company}", new_x="LMARGIN", new_y="NEXT", align="C")
340
+ pdf.set_font("Helvetica", "", 16)
341
+ pdf.cell(0, 10, "AI Agent Security Compliance Report", new_x="LMARGIN", new_y="NEXT", align="C")
342
+ pdf.ln(5)
343
+ pdf.set_font("Helvetica", "", 11)
344
+ pdf.cell(0, 8, f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}", new_x="LMARGIN", new_y="NEXT", align="C")
345
+ pdf.cell(0, 8, f"Audit entries analyzed: {stats['total']:,}", new_x="LMARGIN", new_y="NEXT", align="C")
346
+ pdf.cell(0, 8, f"Actions blocked: {stats['blocked']:,}", new_x="LMARGIN", new_y="NEXT", align="C")
347
+
348
+ # Summary scores
349
+ pdf.ln(10)
350
+ pdf.set_font("Helvetica", "B", 14)
351
+ pdf.cell(0, 10, "Executive Summary", new_x="LMARGIN", new_y="NEXT")
352
+ pdf.set_font("Helvetica", "", 11)
353
+
354
+ for key, report in reports.items():
355
+ passed = report["controls_passed"]
356
+ total = report["controls_total"]
357
+ score = report["score"]
358
+ status = "PASS" if report["pass"] else "FAIL"
359
+ pdf.cell(0, 7, f" {report['framework']}: {passed}/{total} ({score}%) - {status}",
360
+ new_x="LMARGIN", new_y="NEXT")
361
+
362
+ # Detailed results per framework
363
+ for key, report in reports.items():
364
+ pdf.add_page()
365
+ pdf.set_font("Helvetica", "B", 16)
366
+ pdf.cell(0, 12, report["framework"], new_x="LMARGIN", new_y="NEXT")
367
+
368
+ passed = report["controls_passed"]
369
+ total = report["controls_total"]
370
+ score = report["score"]
371
+ pdf.set_font("Helvetica", "", 11)
372
+ pdf.cell(0, 8, f"Score: {passed}/{total} ({score}%)", new_x="LMARGIN", new_y="NEXT")
373
+ pdf.ln(3)
374
+
375
+ # Table header
376
+ pdf.set_font("Helvetica", "B", 9)
377
+ pdf.cell(25, 7, "Control", border=1)
378
+ pdf.cell(40, 7, "Name", border=1)
379
+ pdf.cell(15, 7, "Status", border=1)
380
+ pdf.cell(0, 7, "Evidence", border=1, new_x="LMARGIN", new_y="NEXT")
381
+
382
+ # Table rows
383
+ pdf.set_font("Helvetica", "", 8)
384
+ for ctrl in report["results"]:
385
+ pdf.cell(25, 6, ctrl["id"], border=1)
386
+ pdf.cell(40, 6, ctrl["name"][:25], border=1)
387
+ pdf.cell(15, 6, ctrl["status"], border=1)
388
+ evidence = self._sanitize(ctrl["evidence"][:80])
389
+ pdf.cell(0, 6, evidence, border=1, new_x="LMARGIN", new_y="NEXT")
390
+
391
+ # Footer
392
+ pdf.add_page()
393
+ pdf.set_font("Helvetica", "B", 14)
394
+ pdf.cell(0, 10, "About This Report", new_x="LMARGIN", new_y="NEXT")
395
+ pdf.set_font("Helvetica", "", 10)
396
+ pdf.multi_cell(0, 6, self._sanitize(
397
+ f"This report was automatically generated by {company}'s AI Agent Security Platform. "
398
+ "Controls are evaluated against real audit trail data from production hook enforcement. "
399
+ "Each control status is determined by verifying that the corresponding security mechanism "
400
+ "is active and has processed real events.\n\n"
401
+ f"Platform: SecureMind v4.18.0\n"
402
+ f"Engine: securityagent-core (21,092 lines)\n"
403
+ f"Frameworks: NIST 800-53, SOC 2 Type II, HIPAA, PCI-DSS v4.0\n"
404
+ f"Contact: founders@securemind.live\n"
405
+ f"Website: https://securemind.live"
406
+ ))
407
+
408
+ pdf.output(path)
409
+ return path
410
+
411
+ def _pdf_fallback(self, reports: Dict, stats: Dict, company: str, path: str) -> str:
412
+ """Generate minimal PDF without external dependencies."""
413
+ lines = []
414
+ lines.append(f"{company} - AI Agent Security Compliance Report")
415
+ lines.append(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}")
416
+ lines.append(f"Audit entries: {stats['total']:,} | Blocked: {stats['blocked']:,}")
417
+ lines.append("")
418
+
419
+ for key, report in reports.items():
420
+ passed = report["controls_passed"]
421
+ total = report["controls_total"]
422
+ score = report["score"]
423
+ status = "PASS" if report["pass"] else "FAIL"
424
+ lines.append(f"{'='*60}")
425
+ lines.append(f"{report['framework']}: {passed}/{total} ({score}%) - {status}")
426
+ lines.append(f"{'='*60}")
427
+ for ctrl in report["results"]:
428
+ lines.append(f" [{ctrl['status']}] {ctrl['id']} - {ctrl['name']}")
429
+ lines.append(f" Evidence: {ctrl['evidence']}")
430
+ lines.append("")
431
+
432
+ text = "\n".join(lines)
433
+
434
+ # Minimal valid PDF
435
+ with open(path, "wb") as f:
436
+ objects = []
437
+ # Object 1: Catalog
438
+ objects.append(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")
439
+ # Object 2: Pages
440
+ objects.append(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n")
441
+ # Object 4: Font
442
+ objects.append(b"4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>\nendobj\n")
443
+ # Object 5: Stream content
444
+ content_lines = text.encode("ascii", errors="replace")
445
+ stream = b"BT\n/F1 8 Tf\n36 756 Td\n10 TL\n"
446
+ for line in text.split("\n")[:90]: # Fit one page
447
+ safe = line.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
448
+ stream += f"({safe}) '\n".encode()
449
+ stream += b"ET\n"
450
+ objects.append(f"5 0 obj\n<< /Length {len(stream)} >>\nstream\n".encode() + stream + b"endstream\nendobj\n")
451
+ # Object 3: Page
452
+ objects.append(b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << /Font << /F1 4 0 R >> >> >>\nendobj\n")
453
+
454
+ f.write(b"%PDF-1.4\n")
455
+ offsets = []
456
+ for obj in objects:
457
+ offsets.append(f.tell())
458
+ f.write(obj)
459
+ xref_offset = f.tell()
460
+ f.write(b"xref\n")
461
+ f.write(f"0 {len(objects)+1}\n".encode())
462
+ f.write(b"0000000000 65535 f \n")
463
+ for off in offsets:
464
+ f.write(f"{off:010d} 00000 n \n".encode())
465
+ f.write(b"trailer\n")
466
+ f.write(f"<< /Size {len(objects)+1} /Root 1 0 R >>\n".encode())
467
+ f.write(b"startxref\n")
468
+ f.write(f"{xref_offset}\n".encode())
469
+ f.write(b"%%EOF\n")
470
+
471
+ return path