stride-gpt 0.16.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.
- stride_gpt/__init__.py +1 -0
- stride_gpt/agent/__init__.py +1 -0
- stride_gpt/agent/context.py +167 -0
- stride_gpt/agent/loop.py +617 -0
- stride_gpt/agent/planner.py +188 -0
- stride_gpt/agent/progress.py +168 -0
- stride_gpt/agent/quick.py +198 -0
- stride_gpt/agent/report.py +592 -0
- stride_gpt/agent/tools.py +318 -0
- stride_gpt/cli.py +977 -0
- stride_gpt/config.py +580 -0
- stride_gpt/core/__init__.py +3 -0
- stride_gpt/core/attack_tree.py +222 -0
- stride_gpt/core/dread.py +141 -0
- stride_gpt/core/json_extract.py +47 -0
- stride_gpt/core/llm.py +349 -0
- stride_gpt/core/mitigations.py +39 -0
- stride_gpt/core/prompts/__init__.py +41 -0
- stride_gpt/core/prompts/builder.py +424 -0
- stride_gpt/core/prompts/threat_model/__init__.py +5 -0
- stride_gpt/core/prompts/threat_model/agentic.md +129 -0
- stride_gpt/core/prompts/threat_model/base.md +39 -0
- stride_gpt/core/prompts/threat_model/genai.md +70 -0
- stride_gpt/core/prompts/threat_model/insider_threat.md +120 -0
- stride_gpt/core/prompts/threat_model/quick_base.md +39 -0
- stride_gpt/core/prompts/variants.py +210 -0
- stride_gpt/core/report_utils.py +87 -0
- stride_gpt/core/schemas.py +115 -0
- stride_gpt/core/test_cases.py +46 -0
- stride_gpt/core/threat_model.py +157 -0
- stride_gpt/models.py +355 -0
- stride_gpt/prompt.py +180 -0
- stride_gpt-0.16.0.dist-info/METADATA +489 -0
- stride_gpt-0.16.0.dist-info/RECORD +38 -0
- stride_gpt-0.16.0.dist-info/WHEEL +5 -0
- stride_gpt-0.16.0.dist-info/entry_points.txt +2 -0
- stride_gpt-0.16.0.dist-info/licenses/LICENSE +21 -0
- stride_gpt-0.16.0.dist-info/top_level.txt +1 -0
stride_gpt/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""STRIDE-GPT: AI-powered threat modeling using the STRIDE methodology."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agentic threat modeling — codebase analysis via LLM tool-calling."""
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Context management — token counting and message compression."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import litellm
|
|
9
|
+
|
|
10
|
+
from stride_gpt.core.llm import call_llm
|
|
11
|
+
from stride_gpt.core.schemas import LLMConfig
|
|
12
|
+
from stride_gpt.models import get_model
|
|
13
|
+
|
|
14
|
+
# Default context limits per model family (input tokens).
|
|
15
|
+
# Conservative — leaves room for output.
|
|
16
|
+
DEFAULT_LIMITS: dict[str, int] = {
|
|
17
|
+
"claude": 180_000,
|
|
18
|
+
"gpt": 120_000,
|
|
19
|
+
"gemini": 900_000,
|
|
20
|
+
"llama": 120_000,
|
|
21
|
+
"mistral": 28_000,
|
|
22
|
+
"default": 28_000,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
COMPRESSION_THRESHOLD = 0.80 # Compress when at 80% of limit
|
|
26
|
+
KEEP_RECENT = 6 # Number of recent messages to keep uncompressed
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TokenBudgetSource(Enum):
|
|
30
|
+
"""How the token budget was determined."""
|
|
31
|
+
|
|
32
|
+
QUERIED = "queried" # Got the actual configured value from the provider API
|
|
33
|
+
INFERRED = "inferred" # Guessed from model name
|
|
34
|
+
EXPLICIT = "explicit" # Caller provided context_window directly
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContextManager:
|
|
38
|
+
"""Track token usage and compress messages when approaching the context limit."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: LLMConfig, context_window: int | None = None):
|
|
41
|
+
self.model = config.model_name
|
|
42
|
+
if context_window:
|
|
43
|
+
self.context_window = context_window
|
|
44
|
+
self.budget_source = TokenBudgetSource.EXPLICIT
|
|
45
|
+
else:
|
|
46
|
+
self.context_window, self.budget_source = self._resolve_limit(config)
|
|
47
|
+
|
|
48
|
+
def count_tokens(self, messages: list[dict]) -> int:
|
|
49
|
+
"""Count tokens in a message list using LiteLLM's counter."""
|
|
50
|
+
try:
|
|
51
|
+
return litellm.token_counter(model=self.model, messages=messages)
|
|
52
|
+
except Exception:
|
|
53
|
+
# Rough fallback: ~4 chars per token
|
|
54
|
+
total_chars = sum(len(str(m.get("content", ""))) for m in messages)
|
|
55
|
+
return total_chars // 4
|
|
56
|
+
|
|
57
|
+
def needs_compression(self, messages: list[dict]) -> bool:
|
|
58
|
+
"""Check if messages are approaching the context limit."""
|
|
59
|
+
tokens = self.count_tokens(messages)
|
|
60
|
+
return tokens > int(self.context_window * COMPRESSION_THRESHOLD)
|
|
61
|
+
|
|
62
|
+
def compress(self, config: LLMConfig, messages: list[dict]) -> list[dict]:
|
|
63
|
+
"""Compress older messages by summarizing tool results.
|
|
64
|
+
|
|
65
|
+
Keeps the system prompt, the analysis plan context, and recent messages
|
|
66
|
+
intact. Summarizes everything in between into a condensed findings block.
|
|
67
|
+
|
|
68
|
+
`config` is the LLM used to *perform* the summarization (an architect-
|
|
69
|
+
tier reasoning task). The context window the ContextManager tracks is
|
|
70
|
+
the worker's — that's the model whose conversation we're compressing.
|
|
71
|
+
"""
|
|
72
|
+
if len(messages) <= KEEP_RECENT + 2:
|
|
73
|
+
return messages # Nothing worth compressing
|
|
74
|
+
|
|
75
|
+
# Split: system message(s) at the start, recent messages, middle to compress
|
|
76
|
+
system_msgs = []
|
|
77
|
+
rest = []
|
|
78
|
+
for msg in messages:
|
|
79
|
+
if msg["role"] == "system" and not rest:
|
|
80
|
+
system_msgs.append(msg)
|
|
81
|
+
else:
|
|
82
|
+
rest.append(msg)
|
|
83
|
+
|
|
84
|
+
if len(rest) <= KEEP_RECENT:
|
|
85
|
+
return messages
|
|
86
|
+
|
|
87
|
+
to_compress = rest[:-KEEP_RECENT]
|
|
88
|
+
to_keep = rest[-KEEP_RECENT:]
|
|
89
|
+
|
|
90
|
+
# Build summary of the compressed section
|
|
91
|
+
compressed_text = self._summarize(config, to_compress)
|
|
92
|
+
summary_msg = {
|
|
93
|
+
"role": "user",
|
|
94
|
+
"content": f"[Previous exploration summary]\n{compressed_text}",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return system_msgs + [summary_msg] + to_keep
|
|
98
|
+
|
|
99
|
+
def _summarize(self, config: LLMConfig, messages: list[dict]) -> str:
|
|
100
|
+
"""Summarize a list of messages into key findings."""
|
|
101
|
+
# Build a text representation of the messages
|
|
102
|
+
parts: list[str] = []
|
|
103
|
+
for msg in messages:
|
|
104
|
+
role = msg.get("role", "unknown")
|
|
105
|
+
content = str(msg.get("content", ""))[:2000] # Truncate long entries
|
|
106
|
+
parts.append(f"[{role}] {content}")
|
|
107
|
+
|
|
108
|
+
conversation = "\n---\n".join(parts)
|
|
109
|
+
|
|
110
|
+
summary_messages = [
|
|
111
|
+
{
|
|
112
|
+
"role": "system",
|
|
113
|
+
"content": "Summarize the following agent exploration into a concise list of key findings. Focus on: files examined, security-relevant patterns found, architectural observations, and any threats identified. Be brief but preserve important details.",
|
|
114
|
+
},
|
|
115
|
+
{"role": "user", "content": conversation},
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
response = call_llm(config, summary_messages)
|
|
119
|
+
return response.content
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _resolve_limit(config: LLMConfig) -> tuple[int, TokenBudgetSource]:
|
|
123
|
+
"""Resolve the context token limit for the model.
|
|
124
|
+
|
|
125
|
+
Queries LM Studio for the actual loaded context length.
|
|
126
|
+
Otherwise reads the per-model context window from the registry,
|
|
127
|
+
falling back to name-based inference for unregistered models.
|
|
128
|
+
"""
|
|
129
|
+
if config.provider == "LM Studio Server" and config.api_base:
|
|
130
|
+
result = _query_lm_studio_context(config.api_base, config.model_name)
|
|
131
|
+
if result:
|
|
132
|
+
return result, TokenBudgetSource.QUERIED
|
|
133
|
+
|
|
134
|
+
registered = get_model(config.provider, config.model_name)
|
|
135
|
+
if registered is not None:
|
|
136
|
+
return registered.max_tokens, TokenBudgetSource.QUERIED
|
|
137
|
+
|
|
138
|
+
return _infer_limit_from_name(config.model_name), TokenBudgetSource.INFERRED
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _infer_limit_from_name(model: str) -> int:
|
|
142
|
+
"""Infer context limit from model name keywords."""
|
|
143
|
+
lower = model.lower()
|
|
144
|
+
for key, limit in DEFAULT_LIMITS.items():
|
|
145
|
+
if key in lower:
|
|
146
|
+
return limit
|
|
147
|
+
return DEFAULT_LIMITS["default"]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _query_lm_studio_context(api_base: str, model_name: str) -> int | None:
|
|
151
|
+
"""Query LM Studio's native API for the model's loaded context length."""
|
|
152
|
+
try:
|
|
153
|
+
url = api_base.rstrip("/") + "/api/v1/models"
|
|
154
|
+
resp = httpx.get(url, timeout=5)
|
|
155
|
+
resp.raise_for_status()
|
|
156
|
+
for model in resp.json().get("models", []):
|
|
157
|
+
if model.get("key") != model_name:
|
|
158
|
+
continue
|
|
159
|
+
loaded_instances = model.get("loaded_instances", [])
|
|
160
|
+
if not loaded_instances:
|
|
161
|
+
continue
|
|
162
|
+
loaded_ctx = loaded_instances[0].get("config", {}).get("context_length", 0)
|
|
163
|
+
if loaded_ctx > 0:
|
|
164
|
+
return loaded_ctx
|
|
165
|
+
except (httpx.HTTPError, KeyError, TypeError, ValueError):
|
|
166
|
+
pass
|
|
167
|
+
return None
|