Skip to content

Commit 14705ba

Browse files
committed
Add scenario configuration, instruction parsing, and multi-agent coordination
- Introduced a layered configuration system for the SecNode API, allowing precedence of CLI arguments, scenario files, environment variables, and defaults. - Added functionality to parse instruction sets from CLI arguments, enabling greybox testing with user-defined variables. - Implemented a scenario YAML parser to facilitate greybox API testing workflows, including user and variable definitions. - Developed a multi-agent coordination module to manage parallel execution of tasks, enhancing the API's operational capabilities. - Updated CLI options to support new instruction and scenario features, improving user experience and flexibility.
1 parent acdc29f commit 14705ba

27 files changed

Lines changed: 5924 additions & 9 deletions

.sisyphus/boulder.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"active_plan": "C:\\Users\\admin\\Desktop\\Projects\\CyberIntelligence\\API-PENTESTER\\.sisyphus\\plans\\full-prd-redesign.md",
3+
"started_at": "2026-03-09T02:50:39.528Z",
4+
"session_ids": [
5+
"ses_33186afbfffetz2an8m3P24Kjc"
6+
],
7+
"plan_name": "full-prd-redesign",
8+
"agent": "atlas"
9+
}

.sisyphus/plans/full-prd-redesign.md

Lines changed: 2725 additions & 0 deletions
Large diffs are not rendered by default.

all_tests.txt

Lines changed: 446 additions & 0 deletions
Large diffs are not rendered by default.

src/secnodeapi/agents/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Agents module for SecNode API multi-agent coordination."""
2+
from .coordinator import Agent, AgentCoordinator, Dispatcher
3+
4+
__all__ = [
5+
"Agent",
6+
"AgentCoordinator",
7+
"Dispatcher",
8+
]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Agents module for multi-agent coordination."""
2+
from typing import Any, Dict, List, Optional
3+
4+
5+
class AgentCoordinator:
6+
"""Coordinates multiple agents for parallel execution."""
7+
8+
def __init__(self, max_concurrency: int = 4):
9+
self.max_concurrency = max_concurrency
10+
self.agents: List[" self.results: List[Dict[strAgent"] = []
11+
, Any]] = []
12+
13+
def register_agent(self, agent: "Agent") -> None:
14+
"""Register an agent for coordination.
15+
16+
Args:
17+
agent: Agent instance to register
18+
"""
19+
self.agents.append(agent)
20+
21+
async def coordinate(self, task: Dict[str, Any]) -> List[Dict[str, Any]]:
22+
"""Coordinate agent execution.
23+
24+
Args:
25+
task: Task specification
26+
27+
Returns:
28+
List of results from all agents
29+
"""
30+
# TODO: Implement parallel coordination
31+
# TODO: Handle agent communication
32+
# TODO: Aggregate results
33+
return []
34+
35+
def get_status(self) -> Dict[str, Any]:
36+
"""Get current coordination status.
37+
38+
Returns:
39+
Status information
40+
"""
41+
return {
42+
"registered_agents": len(self.agents),
43+
"max_concurrency": self.max_concurrency,
44+
"results_count": len(self.results),
45+
}
46+
47+
48+
class Agent:
49+
"""Base agent class for task execution."""
50+
51+
def __init__(self, name: str, agent_type: str = "generic"):
52+
self.name = name
53+
self.agent_type = agent_type
54+
self.context: Dict[str, Any] = {}
55+
56+
async def execute(self, task: Dict[str, Any]) -> Dict[str, Any]:
57+
"""Execute a task.
58+
59+
Args:
60+
task: Task specification
61+
62+
Returns:
63+
Execution result
64+
"""
65+
# TODO: Implement agent execution
66+
return {"status": "pending", "agent": self.name}
67+
68+
def update_context(self, context: Dict[str, Any]) -> None:
69+
"""Update agent context.
70+
71+
Args:
72+
context: New context data
73+
"""
74+
self.context.update(context)
75+
76+
77+
class Dispatcher:
78+
"""Dispatches tasks to appropriate agents."""
79+
80+
def __init__(self):
81+
self.agents: Dict[str, Agent] = {}
82+
83+
def register(self, agent_type: str, agent: Agent) -> None:
84+
"""Register an agent for a specific type.
85+
86+
Args:
87+
agent_type: Type identifier for the agent
88+
agent: Agent instance
89+
"""
90+
self.agents[agent_type] = agent
91+
92+
async def dispatch(
93+
self,
94+
task: Dict[str, Any]
95+
) -> Dict[str, Any]:
96+
"""Dispatch task to appropriate agent.
97+
98+
Args:
99+
task: Task to dispatch
100+
101+
Returns:
102+
Task result
103+
"""
104+
agent_type = task.get("agent_type", "generic")
105+
agent = self.agents.get(agent_type)
106+
107+
if agent is None:
108+
return {"error": f"No agent registered for type: {agent_type}"}
109+
110+
return await agent.execute(task)

src/secnodeapi/ai/generate.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
AI test generation stage for adversarial test case creation.
33
"""
44
import json
5-
from typing import List
5+
from typing import List, Optional, Dict, Any
66

77
import structlog
88

@@ -14,7 +14,7 @@
1414

1515

1616
async def generate_test_cases(
17-
understanding: APIUnderstanding, structure: SchemaStructure
17+
understanding: APIUnderstanding, structure: SchemaStructure, instructions: Optional[Dict[str, Any]] = None
1818
) -> List[TestCase]:
1919
"""Generate adversarial test cases for OWASP and business logic flaws."""
2020
logger.info("Generating adversarial test cases with AI")
@@ -42,6 +42,24 @@ async def generate_test_cases(
4242
"Ensure realistic payloads based on the defined schemas (e.g. if an endpoint expects JSON, provide a structured JSON body)."
4343
)
4444

45+
if instructions:
46+
greybox_context = (
47+
"\n\n[GREYBOX CONTEXT - AUTHENTICATED TESTING]\n"
48+
"You have been provided with real credentials and variables for authenticated testing. Use them as follows:\n\n"
49+
"- Include these credentials in HTTP headers (e.g., Authorization: Bearer <token>, X-API-Key: <api_key>)\n"
50+
"- Use real username/password values in request bodies for login/authentication endpoints\n"
51+
"- Apply these values to query parameters where relevant (e.g., user_id, account_id)\n"
52+
"- Generate tests that exercise authenticated API flows, privilege escalation scenarios, BOLA/BFLA attacks, and account enumeration\n\n"
53+
"Available credentials and variables:\n"
54+
)
55+
if isinstance(instructions, dict) and "variables" in instructions:
56+
variables = instructions["variables"]
57+
if isinstance(variables, dict):
58+
for key, value in variables.items():
59+
greybox_context += f"- {key}: {value}\n"
60+
greybox_context += "\nGenerate tests that demonstrate real exploitation using these values."
61+
user_prompt += greybox_context
62+
4563
result = await call_llm(sys_prompt, user_prompt, temperature=0.7)
4664
try:
4765
data = json.loads(result)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Attack graph subsystem."""
22

33
from .engine import AttackGraphEngine
4+
from .networkx_engine import NetworkXAttackGraph, AttackGraphManager
45
from .models import AttackEdge, AttackNode, AttackPath
56

7+
__all__ = ["AttackNode", "AttackEdge", "AttackPath", "AttackGraphEngine", "NetworkXAttackGraph", "AttackGraphManager"]
8+
69
__all__ = ["AttackNode", "AttackEdge", "AttackPath", "AttackGraphEngine"]
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""NetworkX-powered attack graph engine with Postgres persistence."""
2+
from __future__ import annotations
3+
4+
from typing import Any, Dict, List, Optional, Set
5+
import json
6+
7+
import networkx as nx
8+
9+
from .models import AttackEdge, AttackPath
10+
11+
12+
class NetworkXAttackGraph:
13+
"""Attack graph using NetworkX for graph algorithms."""
14+
15+
def __init__(self, session_id: str):
16+
self.session_id = session_id
17+
self._graph: nx.DiGraph = nx.DiGraph()
18+
19+
def add_edge(self, edge: AttackEdge) -> None:
20+
"""Add an edge to the graph."""
21+
self._graph.add_edge(
22+
edge.from_node,
23+
edge.to_node,
24+
vulnerability=edge.vulnerability,
25+
confidence=edge.confidence,
26+
evidence=edge.evidence,
27+
success=edge.success,
28+
)
29+
30+
def nodes(self) -> List[str]:
31+
"""Get all nodes."""
32+
return list(self._graph.nodes())
33+
34+
def edges(self) -> List[AttackEdge]:
35+
"""Get all edges as AttackEdge objects."""
36+
result = []
37+
for from_node, to_node, data in self._graph.edges(data=True):
38+
result.append(AttackEdge(
39+
from_node=from_node,
40+
to_node=to_node,
41+
vulnerability=data.get("vulnerability", ""),
42+
confidence=data.get("confidence", 0.0),
43+
evidence=data.get("evidence", ""),
44+
success=data.get("success", False),
45+
))
46+
return result
47+
48+
def best_paths(self, max_paths: int = 5) -> List[AttackPath]:
49+
"""Find best attack paths using NetworkX algorithms."""
50+
if not nx.is_weakly_connected(self._graph):
51+
# Handle disconnected components
52+
subgraphs = [self._graph.subgraph(c).copy()
53+
for c in nx.weakly_connected_components(self._graph)]
54+
paths = []
55+
for subgraph in subgraphs:
56+
paths.extend(self._find_paths_in_subgraph(subgraph, max_paths))
57+
paths.sort(key=lambda p: p.score, reverse=True)
58+
return paths[:max_paths]
59+
60+
return self._find_paths_in_subgraph(self._graph, max_paths)
61+
62+
def _find_paths_in_subgraph(self, graph: nx.DiGraph, max_paths: int) -> List[AttackPath]:
63+
"""Find paths in a subgraph using NetworkX algorithms."""
64+
all_paths: List[AttackPath] = []
65+
66+
# Find start nodes (nodes with no incoming edges from 'start')
67+
start_nodes = [n for n in graph.nodes() if n == "start" or
68+
not any(graph.predecessors(n))]
69+
70+
for start in start_nodes:
71+
# Use simple DFS to find all simple paths
72+
for node in graph.nodes():
73+
if node == start or start == "start":
74+
try:
75+
for path in nx.all_simple_paths(graph, start, node, cutoff=6):
76+
if len(path) > 1:
77+
edges = []
78+
for i in range(len(path) - 1):
79+
edge_data = graph.get_edge_data(path[i], path[i+1])
80+
if edge_data:
81+
edges.append(AttackEdge(
82+
from_node=path[i],
83+
to_node=path[i+1],
84+
vulnerability=edge_data.get("vulnerability", ""),
85+
confidence=edge_data.get("confidence", 0.0),
86+
evidence=edge_data.get("evidence", ""),
87+
success=edge_data.get("success", False),
88+
))
89+
90+
score = self._score_path(edges)
91+
all_paths.append(AttackPath(
92+
nodes=path,
93+
edges=edges,
94+
score=score,
95+
))
96+
except nx.NetworkXNoPath:
97+
continue
98+
99+
all_paths.sort(key=lambda p: p.score, reverse=True)
100+
return all_paths[:max_paths]
101+
102+
@staticmethod
103+
def _score_path(edges: List[AttackEdge]) -> float:
104+
"""Score a path based on confidence and depth."""
105+
if not edges:
106+
return 0.0
107+
108+
# Filter to only successful edges
109+
successful = [e for e in edges if e.success]
110+
if not successful:
111+
return 0.0
112+
113+
depth_bonus = len(successful) * 0.15
114+
avg_confidence = sum(e.confidence for e in successful) / len(successful)
115+
return avg_confidence + depth_bonus
116+
117+
def successful_nodes(self) -> List[str]:
118+
"""Get nodes reachable via successful edges."""
119+
return [n for n in self._graph.nodes()
120+
if self._graph.in_degree(n) > 0 and
121+
any(self._graph.edges(u, v, data=True).get("success", False)
122+
for u, v in self._graph.in_edges(n))]
123+
124+
def to_dict(self) -> Dict[str, Any]:
125+
"""Serialize graph to dict for Postgres storage."""
126+
return {
127+
"session_id": self.session_id,
128+
"nodes": list(self._graph.nodes()),
129+
"edges": [
130+
{
131+
"from_node": u,
132+
"to_node": v,
133+
**data,
134+
}
135+
for u, v, data in self._graph.edges(data=True)
136+
],
137+
}
138+
139+
@classmethod
140+
def from_dict(cls, data: Dict[str, Any]) -> "NetworkXAttackGraph":
141+
"""Deserialize graph from dict."""
142+
graph = cls(data.get("session_id", ""))
143+
for node in data.get("nodes", []):
144+
graph._graph.add_node(node)
145+
for edge in data.get("edges", []):
146+
graph._graph.add_edge(
147+
edge["from_node"],
148+
edge["to_node"],
149+
vulnerability=edge.get("vulnerability", ""),
150+
confidence=edge.get("confidence", 0.0),
151+
evidence=edge.get("evidence", ""),
152+
success=edge.get("success", False),
153+
)
154+
return graph
155+
156+
157+
class AttackGraphManager:
158+
"""Manages multiple attack graphs with Postgres persistence."""
159+
160+
def __init__(self, storage=None):
161+
"""Initialize with optional Postgres storage."""
162+
self._graphs: Dict[str, NetworkXAttackGraph] = {}
163+
self._storage = storage # Optional storage adapter
164+
165+
def get_graph(self, session_id: str) -> NetworkXAttackGraph:
166+
"""Get or create graph for session."""
167+
if session_id not in self._graphs:
168+
# Try to load from storage
169+
if self._storage:
170+
data = self._storage.load_graph(session_id)
171+
if data:
172+
self._graphs[session_id] = NetworkXAttackGraph.from_dict(data)
173+
else:
174+
self._graphs[session_id] = NetworkXAttackGraph(session_id)
175+
else:
176+
self._graphs[session_id] = NetworkXAttackGraph(session_id)
177+
return self._graphs[session_id]
178+
179+
def save_graph(self, session_id: str) -> None:
180+
"""Persist graph to storage."""
181+
if self._storage and session_id in self._graphs:
182+
self._storage.save_graph(self._graphs[session_id].to_dict())
183+
184+
def delete_graph(self, session_id: str) -> None:
185+
"""Delete graph from memory and storage."""
186+
if session_id in self._graphs:
187+
del self._graphs[session_id]
188+
if self._storage:
189+
self._storage.delete_graph(session_id)

0 commit comments

Comments
 (0)