flintai-cli 1.0.0__py3-none-any.whl

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 (147) hide show
  1. flintai/__init__.py +0 -0
  2. flintai/cli/__init__.py +0 -0
  3. flintai/cli/__main__.py +3 -0
  4. flintai/cli/builtin_config.json +911 -0
  5. flintai/cli/console.py +116 -0
  6. flintai/cli/eval_cli.py +891 -0
  7. flintai/cli/init_cli.py +144 -0
  8. flintai/cli/main.py +262 -0
  9. flintai/cli/rich_observer.py +89 -0
  10. flintai/cli/runner.py +305 -0
  11. flintai/cli/scan_cli.py +238 -0
  12. flintai/cli/utils.py +37 -0
  13. flintai/cli/version.py +1 -0
  14. flintai/data/detector_prompts/llm01_adversarial.txt +55 -0
  15. flintai/data/detector_prompts/llm01_fixed.txt +8 -0
  16. flintai/data/detector_prompts/llm02_adversarial.txt +75 -0
  17. flintai/data/detector_prompts/llm02_fixed.txt +8 -0
  18. flintai/data/detector_prompts/llm05_adversarial.txt +63 -0
  19. flintai/data/detector_prompts/llm05_fixed.txt +16 -0
  20. flintai/data/detector_prompts/llm06_adversarial.txt +135 -0
  21. flintai/data/detector_prompts/llm06_fixed.txt +15 -0
  22. flintai/data/detector_prompts/llm07_adversarial.txt +185 -0
  23. flintai/data/detector_prompts/llm07_fixed.txt +14 -0
  24. flintai/data/detector_prompts/llm09_adversarial.txt +77 -0
  25. flintai/data/detector_prompts/llm09_fixed.txt +15 -0
  26. flintai/data/llm01_prompt_injection.csv +6708 -0
  27. flintai/data/llm02_sensitive_information.csv +1069 -0
  28. flintai/data/llm05_unsafe_output.csv +3882 -0
  29. flintai/data/llm06_excessive_agency.csv +778 -0
  30. flintai/data/llm07_system_prompt_leakage.csv +971 -0
  31. flintai/data/llm09_hallucination-goals.csv +32419 -0
  32. flintai/data/llm09_hallucination.csv +599 -0
  33. flintai/data/pii_leakage.csv +706 -0
  34. flintai/data/secret_leakage.csv +1380 -0
  35. flintai/eval/__init__.py +0 -0
  36. flintai/eval/common/converter_anthropic.py +151 -0
  37. flintai/eval/common/converter_genai.py +169 -0
  38. flintai/eval/common/converter_openai.py +165 -0
  39. flintai/eval/common/log.py +96 -0
  40. flintai/eval/common/reference.py +25 -0
  41. flintai/eval/common/schema.py +192 -0
  42. flintai/eval/common/utils.py +74 -0
  43. flintai/eval/core/__init__.py +0 -0
  44. flintai/eval/core/detectors/__init__.py +1 -0
  45. flintai/eval/core/detectors/detector.py +25 -0
  46. flintai/eval/core/detectors/detector_garak.py +80 -0
  47. flintai/eval/core/detectors/detector_model.py +76 -0
  48. flintai/eval/core/detectors/detector_pii.py +64 -0
  49. flintai/eval/core/detectors/detector_secret.py +82 -0
  50. flintai/eval/core/detectors/detector_topic_guard.py +70 -0
  51. flintai/eval/core/detectors/detector_toxicity.py +70 -0
  52. flintai/eval/core/eval/__init__.py +0 -0
  53. flintai/eval/core/eval/eval_creator.py +197 -0
  54. flintai/eval/core/eval/evaluation.py +100 -0
  55. flintai/eval/core/eval/evaluation_adversarial.py +523 -0
  56. flintai/eval/core/eval/evaluation_garak_module.py +52 -0
  57. flintai/eval/core/eval/evaluation_garak_probe.py +274 -0
  58. flintai/eval/core/eval/evaluation_message_list.py +44 -0
  59. flintai/eval/core/eval/evaluation_multi.py +207 -0
  60. flintai/eval/core/eval/evaluation_single.py +79 -0
  61. flintai/eval/core/eval/evaluation_single_prompt.py +58 -0
  62. flintai/eval/core/eval/evaluation_topic_guard.py +318 -0
  63. flintai/eval/core/eval/metric_conciseness.py +181 -0
  64. flintai/eval/core/eval/metric_factual_accuracy.py +294 -0
  65. flintai/eval/core/eval/metric_instruction_adherence.py +180 -0
  66. flintai/eval/core/eval/metric_tone.py +183 -0
  67. flintai/eval/core/eval/metric_toxicity.py +147 -0
  68. flintai/eval/core/eval/observer.py +136 -0
  69. flintai/eval/core/eval/probe_adversarial.py +14 -0
  70. flintai/eval/core/message/__init__.py +0 -0
  71. flintai/eval/core/message/message_collection.py +37 -0
  72. flintai/eval/core/message/message_collection_csv.py +46 -0
  73. flintai/eval/core/message/message_collection_garak.py +52 -0
  74. flintai/eval/core/message/message_collection_memory.py +36 -0
  75. flintai/eval/core/models/__init__.py +0 -0
  76. flintai/eval/core/models/generator_model.py +88 -0
  77. flintai/eval/core/models/model.py +111 -0
  78. flintai/eval/core/models/model_adk.py +158 -0
  79. flintai/eval/core/models/model_anthropic.py +51 -0
  80. flintai/eval/core/models/model_anthropic_agent.py +92 -0
  81. flintai/eval/core/models/model_gemini.py +107 -0
  82. flintai/eval/core/models/model_generic_http.py +117 -0
  83. flintai/eval/core/models/model_huggingface.py +61 -0
  84. flintai/eval/core/models/model_langserve.py +94 -0
  85. flintai/eval/core/models/model_litellm.py +38 -0
  86. flintai/eval/core/models/model_ollama.py +50 -0
  87. flintai/eval/core/models/model_openai.py +35 -0
  88. flintai/eval/core/models/model_openai_agent.py +80 -0
  89. flintai/eval/core/models/model_openai_compatible.py +57 -0
  90. flintai/eval/core/models/model_retry.py +134 -0
  91. flintai/eval/core/models/model_sync_wrapper.py +35 -0
  92. flintai/eval/db/__init__.py +0 -0
  93. flintai/eval/db/base/__init__.py +0 -0
  94. flintai/eval/db/base/detectors/__init__.py +1 -0
  95. flintai/eval/db/base/detectors/detector_helpers.py +78 -0
  96. flintai/eval/db/base/detectors/detector_repository.py +91 -0
  97. flintai/eval/db/base/detectors/detector_types.py +77 -0
  98. flintai/eval/db/base/eval/__init__.py +1 -0
  99. flintai/eval/db/base/eval/eval_helpers.py +227 -0
  100. flintai/eval/db/base/eval/eval_repository.py +115 -0
  101. flintai/eval/db/base/eval/eval_run.py +139 -0
  102. flintai/eval/db/base/eval/eval_types.py +129 -0
  103. flintai/eval/db/base/eval/model_eval_repository.py +78 -0
  104. flintai/eval/db/base/eval/model_eval_run_repository.py +70 -0
  105. flintai/eval/db/base/eval/model_eval_run_result_repository.py +40 -0
  106. flintai/eval/db/base/eval/model_eval_run_result_types.py +21 -0
  107. flintai/eval/db/base/eval/model_eval_run_types.py +84 -0
  108. flintai/eval/db/base/eval/model_eval_types.py +71 -0
  109. flintai/eval/db/base/message/__init__.py +0 -0
  110. flintai/eval/db/base/message/message_collection_helpers.py +61 -0
  111. flintai/eval/db/base/message/message_collection_repository.py +92 -0
  112. flintai/eval/db/base/message/message_collection_types.py +89 -0
  113. flintai/eval/db/base/models/__init__.py +0 -0
  114. flintai/eval/db/base/models/model_helpers.py +172 -0
  115. flintai/eval/db/base/models/model_repository.py +95 -0
  116. flintai/eval/db/base/models/model_types.py +156 -0
  117. flintai/eval/db/json/__init__.py +0 -0
  118. flintai/eval/db/json/repository_json.py +603 -0
  119. flintai/scan/__init__.py +0 -0
  120. flintai/scan/agent_scanner.py +797 -0
  121. flintai/scan/config/agent_opengrep_rules.yaml +533 -0
  122. flintai/scan/config/agent_taxonomy.json +481 -0
  123. flintai/scan/config/agentic_cvss_mapping.yaml +136 -0
  124. flintai/scan/config/compliance_mappings.json +110 -0
  125. flintai/scan/config/triage_prompt.txt +335 -0
  126. flintai/scan/constants.py +15 -0
  127. flintai/scan/file_filter.py +156 -0
  128. flintai/scan/llm_provider.py +197 -0
  129. flintai/scan/opengrep_resolver.py +12 -0
  130. flintai/scan/reasoner.py +552 -0
  131. flintai/scan/schema.py +337 -0
  132. flintai/scan/scorer.py +371 -0
  133. flintai/scan/secret_anonymizer.py +72 -0
  134. flintai/scan/static_scanner.py +542 -0
  135. flintai/scan/taxonomy.py +75 -0
  136. flintai/scan/tool_dispatcher.py +754 -0
  137. flintai/scan/trace_logger.py +118 -0
  138. flintai/scan/trace_logger_file.py +143 -0
  139. flintai/scan/trace_logger_log.py +100 -0
  140. flintai/scan/triage.py +496 -0
  141. flintai/schema.py +58 -0
  142. flintai_cli-1.0.0.dist-info/METADATA +442 -0
  143. flintai_cli-1.0.0.dist-info/RECORD +147 -0
  144. flintai_cli-1.0.0.dist-info/WHEEL +5 -0
  145. flintai_cli-1.0.0.dist-info/entry_points.txt +2 -0
  146. flintai_cli-1.0.0.dist-info/licenses/LICENSE +210 -0
  147. flintai_cli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,118 @@
1
+ """
2
+ trace_logger.py — Structured Observability for the Agentic Reasoning Loop
3
+ =========================================================================
4
+ Writes a JSONL trace log of every tool call made during an agentic scan session.
5
+
6
+ WHY THIS EXISTS:
7
+ The agentic scanner itself exhibits the behaviours it scans for in customer agents:
8
+ autonomous tool-calling, adaptive control flow, incremental decision-making.
9
+ Per ASI10 (missing_agent_monitoring), any agent that takes consequential actions
10
+ without an audit trail is a security risk. This module is the scanner's own
11
+ compliance with that rule.
12
+
13
+ OUTPUT FORMAT:
14
+ One JSON object per line, written to <output_path>.trace.jsonl
15
+ Each line represents one tool call with timing, token estimates, and a result preview.
16
+ The final line is a session summary record.
17
+
18
+ USAGE:
19
+ logger = FileTraceLogger(session_id="AGT-SCAN-abc123", output_path="report.json")
20
+ logger.start(provider_model="claude-sonnet-4-6")
21
+
22
+ with logger.record_call("fetch_file", {"path": "tools/exec.py"}) as ctx:
23
+ result = do_fetch(...)
24
+ ctx.set_result(result)
25
+
26
+ logger.finish(findings_count=5, exit_reason="completed")
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import abc
32
+ from collections.abc import Iterator
33
+ from contextlib import contextmanager
34
+ from datetime import datetime, timezone
35
+
36
+ import tiktoken
37
+
38
+ # ── Token estimation ──────────────────────────────────────────────────────────
39
+
40
+ _TOKEN_REFERENCE_MODEL = "gpt-4o"
41
+
42
+
43
+ def estimate_tokens(text: str) -> int:
44
+ """Estimate token count using tiktoken (accurate) or char heuristic (fallback).
45
+
46
+ Returns:
47
+ Estimated number of tokens (minimum 1).
48
+ """
49
+ try:
50
+ return max(
51
+ 1, len(tiktoken.encoding_for_model(_TOKEN_REFERENCE_MODEL).encode(text))
52
+ )
53
+ except Exception:
54
+ return max(1, len(text) // 4)
55
+
56
+
57
+ def now_iso() -> str:
58
+ """Return the current UTC time as an ISO 8601 string."""
59
+ return datetime.now(timezone.utc).isoformat()
60
+
61
+
62
+ class CallContext:
63
+ """Mutable context shared between the record_call context manager and the caller."""
64
+
65
+ def __init__(self):
66
+ self.result: str | None = None
67
+ self.success: bool = True
68
+ self.error: str | None = None
69
+
70
+ def set_result(self, result: str) -> None:
71
+ self.result = result
72
+
73
+ def set_error(self, error: str) -> None:
74
+ self.error = error
75
+ self.success = False
76
+ self.result = f"ERROR: {error}"
77
+
78
+
79
+ # ── Abstract interface ────────────────────────────────────────────────────────
80
+
81
+
82
+ class TraceLogger(abc.ABC):
83
+ """
84
+ Abstract interface for structured trace logging of tool calls
85
+ during an agentic scan session.
86
+
87
+ Thread-safety: single-threaded use only (scanner runs synchronously).
88
+ """
89
+
90
+ @abc.abstractmethod
91
+ def start(self, provider_model: str) -> None:
92
+ """Begin a trace session. Call once before the reasoning loop starts."""
93
+
94
+ @abc.abstractmethod
95
+ @contextmanager
96
+ def record_call(
97
+ self, tool_name: str, tool_args: dict, iteration: int
98
+ ) -> Iterator[CallContext]:
99
+ """Context manager that times a tool call and writes the trace entry.
100
+
101
+ Usage:
102
+ with logger.record_call("fetch_file", {"path": "x.py"}, iteration=2) as ctx:
103
+ result = fetch_file(...)
104
+ ctx.set_result(result)
105
+ """
106
+ yield CallContext() # pragma: no cover
107
+
108
+ @abc.abstractmethod
109
+ def set_iteration(self, n: int) -> None:
110
+ """Update the current iteration count."""
111
+
112
+ @abc.abstractmethod
113
+ def finish(self, findings_count: int, exit_reason: str = "completed") -> None:
114
+ """Write the session summary and close. Call once after the reasoning loop exits."""
115
+
116
+ @abc.abstractmethod
117
+ def as_dict(self) -> dict:
118
+ """Return a summary dict suitable for embedding in ScanReport.scan_metadata."""
@@ -0,0 +1,143 @@
1
+ """FileTraceLogger — writes a JSONL trace file to disk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import time
8
+ import uuid
9
+ from contextlib import contextmanager
10
+
11
+ from flintai.scan.trace_logger import (
12
+ CallContext,
13
+ TraceLogger,
14
+ estimate_tokens,
15
+ now_iso,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class FileTraceLogger(TraceLogger):
22
+ """
23
+ Writes a structured JSONL trace of all tool calls made during an agentic scan.
24
+
25
+ Output is written to ``<output_path>.trace.jsonl``.
26
+ """
27
+
28
+ def __init__(self, session_id: str | None = None, output_path: str | None = None):
29
+ self.session_id = session_id or f"AGT-SCAN-{uuid.uuid4().hex[:8].upper()}"
30
+ self.output_path = output_path
31
+ self._trace_path = f"{output_path}.trace.jsonl" if output_path else None
32
+ self._start_time: float | None = None
33
+ self._provider = "unknown"
34
+ self._iterations = 0
35
+ self._call_count = 0
36
+ self._total_tokens = 0
37
+ self._tool_calls: list[dict] = []
38
+ self._file = None
39
+
40
+ def start(self, provider_model: str) -> None:
41
+ self._start_time = time.monotonic()
42
+ self._provider = provider_model
43
+ self._write_line(
44
+ {
45
+ "event": "session_start",
46
+ "session_id": self.session_id,
47
+ "model": provider_model,
48
+ "timestamp": now_iso(),
49
+ }
50
+ )
51
+ logger.info(
52
+ "Trace session started: %s | model: %s", self.session_id, provider_model
53
+ )
54
+
55
+ @contextmanager
56
+ def record_call(self, tool_name: str, tool_args: dict, iteration: int):
57
+ call_start = time.monotonic()
58
+ ctx = CallContext()
59
+ try:
60
+ yield ctx
61
+ finally:
62
+ duration_ms = int((time.monotonic() - call_start) * 1000)
63
+ result_text = ctx.result or ""
64
+ tokens = estimate_tokens(result_text)
65
+ self._total_tokens += tokens
66
+ self._call_count += 1
67
+
68
+ entry = {
69
+ "event": "tool_call",
70
+ "session_id": self.session_id,
71
+ "iteration": iteration,
72
+ "call_number": self._call_count,
73
+ "tool": tool_name,
74
+ "args": tool_args,
75
+ "result_preview": result_text[:200],
76
+ "result_length": len(result_text),
77
+ "tokens_consumed": tokens,
78
+ "tokens_total": self._total_tokens,
79
+ "duration_ms": duration_ms,
80
+ "timestamp": now_iso(),
81
+ "success": ctx.success,
82
+ "error": ctx.error,
83
+ }
84
+ self._tool_calls.append(entry)
85
+ self._write_line(entry)
86
+
87
+ def set_iteration(self, n: int) -> None:
88
+ self._iterations = n
89
+
90
+ def finish(self, findings_count: int, exit_reason: str = "completed") -> None:
91
+ if self._start_time is None:
92
+ return
93
+ wall_ms = int((time.monotonic() - self._start_time) * 1000)
94
+ summary = {
95
+ "event": "session_end",
96
+ "session_id": self.session_id,
97
+ "model": self._provider,
98
+ "exit_reason": exit_reason,
99
+ "total_iterations": self._iterations,
100
+ "total_tool_calls": self._call_count,
101
+ "total_tokens": self._total_tokens,
102
+ "wall_clock_ms": wall_ms,
103
+ "findings_count": findings_count,
104
+ "timestamp": now_iso(),
105
+ }
106
+ self._write_line(summary)
107
+ if self._file:
108
+ self._file.close()
109
+ self._file = None
110
+ logger.info(
111
+ "Trace session complete: %s | iterations=%d | calls=%d | "
112
+ "tokens≈%d | %dms | exit=%s",
113
+ self.session_id,
114
+ self._iterations,
115
+ self._call_count,
116
+ self._total_tokens,
117
+ wall_ms,
118
+ exit_reason,
119
+ )
120
+ if self._trace_path:
121
+ logger.info("Trace written to: %s", self._trace_path)
122
+
123
+ def as_dict(self) -> dict:
124
+ return {
125
+ "session_id": self.session_id,
126
+ "provider_model": self._provider,
127
+ "total_iterations": self._iterations,
128
+ "total_tool_calls": self._call_count,
129
+ "total_tokens": self._total_tokens,
130
+ }
131
+
132
+ def _write_line(self, obj: dict) -> None:
133
+ """Append a JSON line to the trace file. Opens on first write."""
134
+ if self._trace_path is None:
135
+ return
136
+ try:
137
+ if self._file is None:
138
+ self._file = open(self._trace_path, "w", encoding="utf-8")
139
+ self._file.write(json.dumps(obj) + "\n")
140
+ self._file.flush()
141
+ except Exception as e:
142
+ # Trace writes are best-effort — never crash the scanner
143
+ logger.warning("Could not write trace entry: %s", e)
@@ -0,0 +1,100 @@
1
+ """LogTraceLogger — emits trace events via Python logging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ import uuid
8
+ from contextlib import contextmanager
9
+
10
+ from flintai.scan.trace_logger import (
11
+ CallContext,
12
+ TraceLogger,
13
+ estimate_tokens,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class LogTraceLogger(TraceLogger):
20
+ """
21
+ Trace logger that emits structured information via Python's logging module
22
+ instead of writing to a file. Useful for environments where file output
23
+ is unavailable or when trace data should flow through the standard
24
+ log pipeline.
25
+ """
26
+
27
+ def __init__(self, session_id: str | None = None, log_level: int = logging.INFO):
28
+ self.session_id = session_id or f"AGT-SCAN-{uuid.uuid4().hex[:8].upper()}"
29
+ self._log_level = log_level
30
+ self._start_time: float | None = None
31
+ self._provider = "unknown"
32
+ self._iterations = 0
33
+ self._call_count = 0
34
+ self._total_tokens = 0
35
+
36
+ def start(self, provider_model: str) -> None:
37
+ self._start_time = time.monotonic()
38
+ self._provider = provider_model
39
+ logger.log(
40
+ self._log_level,
41
+ "[trace:%s] session_start | model=%s",
42
+ self.session_id,
43
+ provider_model,
44
+ )
45
+
46
+ @contextmanager
47
+ def record_call(self, tool_name: str, tool_args: dict, iteration: int):
48
+ call_start = time.monotonic()
49
+ ctx = CallContext()
50
+ try:
51
+ yield ctx
52
+ finally:
53
+ duration_ms = int((time.monotonic() - call_start) * 1000)
54
+ result_text = ctx.result or ""
55
+ tokens = estimate_tokens(result_text)
56
+ self._total_tokens += tokens
57
+ self._call_count += 1
58
+
59
+ logger.log(
60
+ self._log_level,
61
+ "[trace:%s] tool_call #%d | iter=%d tool=%s "
62
+ "duration=%dms tokens=%d success=%s%s",
63
+ self.session_id,
64
+ self._call_count,
65
+ iteration,
66
+ tool_name,
67
+ duration_ms,
68
+ tokens,
69
+ ctx.success,
70
+ f" error={ctx.error}" if ctx.error else "",
71
+ )
72
+
73
+ def set_iteration(self, n: int) -> None:
74
+ self._iterations = n
75
+
76
+ def finish(self, findings_count: int, exit_reason: str = "completed") -> None:
77
+ if self._start_time is None:
78
+ return
79
+ wall_ms = int((time.monotonic() - self._start_time) * 1000)
80
+ logger.log(
81
+ self._log_level,
82
+ "[trace:%s] session_end | exit=%s iterations=%d calls=%d "
83
+ "tokens≈%d wall=%dms findings=%d",
84
+ self.session_id,
85
+ exit_reason,
86
+ self._iterations,
87
+ self._call_count,
88
+ self._total_tokens,
89
+ wall_ms,
90
+ findings_count,
91
+ )
92
+
93
+ def as_dict(self) -> dict:
94
+ return {
95
+ "session_id": self.session_id,
96
+ "provider_model": self._provider,
97
+ "total_iterations": self._iterations,
98
+ "total_tool_calls": self._call_count,
99
+ "total_tokens": self._total_tokens,
100
+ }