diagram-to-iac 0.6.0__py3-none-any.whl → 0.8.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 (77) hide show
  1. diagram_to_iac/__init__.py +10 -0
  2. diagram_to_iac/actions/__init__.py +7 -0
  3. diagram_to_iac/actions/git_entry.py +174 -0
  4. diagram_to_iac/actions/supervisor_entry.py +116 -0
  5. diagram_to_iac/actions/terraform_agent_entry.py +207 -0
  6. diagram_to_iac/agents/__init__.py +26 -0
  7. diagram_to_iac/agents/demonstrator_langgraph/__init__.py +10 -0
  8. diagram_to_iac/agents/demonstrator_langgraph/agent.py +826 -0
  9. diagram_to_iac/agents/git_langgraph/__init__.py +10 -0
  10. diagram_to_iac/agents/git_langgraph/agent.py +1018 -0
  11. diagram_to_iac/agents/git_langgraph/pr.py +146 -0
  12. diagram_to_iac/agents/hello_langgraph/__init__.py +9 -0
  13. diagram_to_iac/agents/hello_langgraph/agent.py +621 -0
  14. diagram_to_iac/agents/policy_agent/__init__.py +15 -0
  15. diagram_to_iac/agents/policy_agent/agent.py +507 -0
  16. diagram_to_iac/agents/policy_agent/integration_example.py +191 -0
  17. diagram_to_iac/agents/policy_agent/tools/__init__.py +14 -0
  18. diagram_to_iac/agents/policy_agent/tools/tfsec_tool.py +259 -0
  19. diagram_to_iac/agents/shell_langgraph/__init__.py +21 -0
  20. diagram_to_iac/agents/shell_langgraph/agent.py +122 -0
  21. diagram_to_iac/agents/shell_langgraph/detector.py +50 -0
  22. diagram_to_iac/agents/supervisor_langgraph/__init__.py +17 -0
  23. diagram_to_iac/agents/supervisor_langgraph/agent.py +1947 -0
  24. diagram_to_iac/agents/supervisor_langgraph/demonstrator.py +22 -0
  25. diagram_to_iac/agents/supervisor_langgraph/guards.py +23 -0
  26. diagram_to_iac/agents/supervisor_langgraph/pat_loop.py +49 -0
  27. diagram_to_iac/agents/supervisor_langgraph/router.py +9 -0
  28. diagram_to_iac/agents/terraform_langgraph/__init__.py +15 -0
  29. diagram_to_iac/agents/terraform_langgraph/agent.py +1216 -0
  30. diagram_to_iac/agents/terraform_langgraph/parser.py +76 -0
  31. diagram_to_iac/core/__init__.py +7 -0
  32. diagram_to_iac/core/agent_base.py +19 -0
  33. diagram_to_iac/core/enhanced_memory.py +302 -0
  34. diagram_to_iac/core/errors.py +4 -0
  35. diagram_to_iac/core/issue_tracker.py +49 -0
  36. diagram_to_iac/core/memory.py +132 -0
  37. diagram_to_iac/services/__init__.py +10 -0
  38. diagram_to_iac/services/observability.py +59 -0
  39. diagram_to_iac/services/step_summary.py +77 -0
  40. diagram_to_iac/tools/__init__.py +11 -0
  41. diagram_to_iac/tools/api_utils.py +108 -26
  42. diagram_to_iac/tools/git/__init__.py +45 -0
  43. diagram_to_iac/tools/git/git.py +956 -0
  44. diagram_to_iac/tools/hello/__init__.py +30 -0
  45. diagram_to_iac/tools/hello/cal_utils.py +31 -0
  46. diagram_to_iac/tools/hello/text_utils.py +97 -0
  47. diagram_to_iac/tools/llm_utils/__init__.py +20 -0
  48. diagram_to_iac/tools/llm_utils/anthropic_driver.py +87 -0
  49. diagram_to_iac/tools/llm_utils/base_driver.py +90 -0
  50. diagram_to_iac/tools/llm_utils/gemini_driver.py +89 -0
  51. diagram_to_iac/tools/llm_utils/openai_driver.py +93 -0
  52. diagram_to_iac/tools/llm_utils/router.py +303 -0
  53. diagram_to_iac/tools/sec_utils.py +4 -2
  54. diagram_to_iac/tools/shell/__init__.py +17 -0
  55. diagram_to_iac/tools/shell/shell.py +415 -0
  56. diagram_to_iac/tools/text_utils.py +277 -0
  57. diagram_to_iac/tools/tf/terraform.py +851 -0
  58. diagram_to_iac-0.8.0.dist-info/METADATA +99 -0
  59. diagram_to_iac-0.8.0.dist-info/RECORD +64 -0
  60. {diagram_to_iac-0.6.0.dist-info → diagram_to_iac-0.8.0.dist-info}/WHEEL +1 -1
  61. diagram_to_iac-0.8.0.dist-info/entry_points.txt +4 -0
  62. diagram_to_iac/agents/codegen_agent.py +0 -0
  63. diagram_to_iac/agents/consensus_agent.py +0 -0
  64. diagram_to_iac/agents/deployment_agent.py +0 -0
  65. diagram_to_iac/agents/github_agent.py +0 -0
  66. diagram_to_iac/agents/interpretation_agent.py +0 -0
  67. diagram_to_iac/agents/question_agent.py +0 -0
  68. diagram_to_iac/agents/supervisor.py +0 -0
  69. diagram_to_iac/agents/vision_agent.py +0 -0
  70. diagram_to_iac/core/config.py +0 -0
  71. diagram_to_iac/tools/cv_utils.py +0 -0
  72. diagram_to_iac/tools/gh_utils.py +0 -0
  73. diagram_to_iac/tools/tf_utils.py +0 -0
  74. diagram_to_iac-0.6.0.dist-info/METADATA +0 -16
  75. diagram_to_iac-0.6.0.dist-info/RECORD +0 -32
  76. diagram_to_iac-0.6.0.dist-info/entry_points.txt +0 -2
  77. {diagram_to_iac-0.6.0.dist-info → diagram_to_iac-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,14 @@
1
+ """
2
+ Policy Agent Tools
3
+
4
+ This module provides security policy enforcement tools for the PolicyAgent.
5
+ """
6
+
7
+ from .tfsec_tool import TfSecTool, TfSecScanInput, TfSecScanOutput, TfSecFinding
8
+
9
+ __all__ = [
10
+ "TfSecTool",
11
+ "TfSecScanInput",
12
+ "TfSecScanOutput",
13
+ "TfSecFinding"
14
+ ]
@@ -0,0 +1,259 @@
1
+ """
2
+ TfSec Tool - Policy scanning tool for Terraform configurations
3
+
4
+ This tool provides secure execution of tfsec policy scanning following
5
+ the established security patterns in the diagram-to-iac system.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Dict, List, Any, Optional
13
+ from pydantic import BaseModel, Field
14
+
15
+ from diagram_to_iac.tools.shell.shell import ShellExecutor, ShellExecInput
16
+
17
+
18
+ class TfSecScanInput(BaseModel):
19
+ """Input for tfsec security scan."""
20
+ repo_path: str = Field(..., description="Path to Terraform repository to scan")
21
+ output_format: str = Field(default="json", description="Output format (json, text)")
22
+ severity_filter: Optional[List[str]] = Field(default=None, description="Filter by severity levels")
23
+ timeout: int = Field(default=120, description="Timeout in seconds for scan")
24
+
25
+
26
+ class TfSecFinding(BaseModel):
27
+ """Represents a single tfsec finding."""
28
+ rule_id: str = Field(..., description="tfsec rule ID")
29
+ rule_description: str = Field(..., description="Description of the rule")
30
+ severity: str = Field(..., description="Severity level (CRITICAL, HIGH, MEDIUM, LOW)")
31
+ resource: str = Field(..., description="Terraform resource affected")
32
+ location: Dict[str, Any] = Field(..., description="Location information")
33
+ description: str = Field(..., description="Finding description")
34
+
35
+
36
+ class TfSecScanOutput(BaseModel):
37
+ """Output from tfsec security scan."""
38
+ scan_successful: bool = Field(..., description="Whether scan completed successfully")
39
+ findings: List[TfSecFinding] = Field(default_factory=list, description="Security findings")
40
+ total_findings: int = Field(default=0, description="Total number of findings")
41
+ critical_count: int = Field(default=0, description="Number of critical findings")
42
+ high_count: int = Field(default=0, description="Number of high severity findings")
43
+ medium_count: int = Field(default=0, description="Number of medium severity findings")
44
+ low_count: int = Field(default=0, description="Number of low severity findings")
45
+ scan_duration: float = Field(default=0.0, description="Scan duration in seconds")
46
+ raw_output: str = Field(default="", description="Raw tfsec output")
47
+ error_message: Optional[str] = Field(default=None, description="Error message if scan failed")
48
+
49
+
50
+ class TfSecTool:
51
+ """
52
+ TfSec tool for Terraform security scanning.
53
+
54
+ This tool provides secure execution of tfsec following the established
55
+ security patterns in the diagram-to-iac system.
56
+ """
57
+
58
+ def __init__(self, config_path: str = None):
59
+ """Initialize TfSec tool with security executor."""
60
+ self.shell_executor = ShellExecutor(config_path=config_path)
61
+
62
+ def scan(self, scan_input: TfSecScanInput) -> TfSecScanOutput:
63
+ """
64
+ Execute tfsec security scan on Terraform repository.
65
+
66
+ Args:
67
+ scan_input: TfSec scan configuration
68
+
69
+ Returns:
70
+ TfSecScanOutput: Scan results and findings
71
+ """
72
+ start_time = time.time()
73
+
74
+ try:
75
+ # Validate repository path exists
76
+ repo_path = Path(scan_input.repo_path)
77
+ if not repo_path.exists():
78
+ return TfSecScanOutput(
79
+ scan_successful=False,
80
+ error_message=f"Repository path does not exist: {scan_input.repo_path}"
81
+ )
82
+
83
+ # Build tfsec command
84
+ command = self._build_tfsec_command(scan_input)
85
+
86
+ # Execute scan using secure shell executor
87
+ shell_input = ShellExecInput(
88
+ command=command,
89
+ cwd=scan_input.repo_path,
90
+ timeout=scan_input.timeout
91
+ )
92
+
93
+ shell_result = self.shell_executor.shell_exec(shell_input)
94
+ scan_duration = time.time() - start_time
95
+
96
+ # Parse tfsec output
97
+ if shell_result.exit_code == 0 or shell_result.exit_code == 1:
98
+ # Exit code 1 is normal for tfsec when findings are present
99
+ return self._parse_tfsec_output(
100
+ shell_result.output,
101
+ scan_duration,
102
+ True
103
+ )
104
+ else:
105
+ return TfSecScanOutput(
106
+ scan_successful=False,
107
+ scan_duration=scan_duration,
108
+ raw_output=shell_result.output,
109
+ error_message=f"tfsec scan failed with exit code {shell_result.exit_code}"
110
+ )
111
+
112
+ except Exception as e:
113
+ scan_duration = time.time() - start_time
114
+ return TfSecScanOutput(
115
+ scan_successful=False,
116
+ scan_duration=scan_duration,
117
+ error_message=f"tfsec scan error: {str(e)}"
118
+ )
119
+
120
+ def _build_tfsec_command(self, scan_input: TfSecScanInput) -> str:
121
+ """Build tfsec command with appropriate options."""
122
+ command_parts = ["tfsec"]
123
+
124
+ # Add output format
125
+ command_parts.extend(["--format", scan_input.output_format])
126
+
127
+ # Add severity filter if specified
128
+ if scan_input.severity_filter:
129
+ for severity in scan_input.severity_filter:
130
+ command_parts.extend(["--minimum-severity", severity.upper()])
131
+
132
+ # Add current directory (will be set by shell executor)
133
+ command_parts.append(".")
134
+
135
+ return " ".join(command_parts)
136
+
137
+ def _parse_tfsec_output(
138
+ self,
139
+ raw_output: str,
140
+ scan_duration: float,
141
+ scan_successful: bool
142
+ ) -> TfSecScanOutput:
143
+ """Parse tfsec JSON output into structured format."""
144
+ findings = []
145
+ counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
146
+
147
+ try:
148
+ if raw_output.strip():
149
+ # Parse JSON output from tfsec
150
+ tfsec_data = json.loads(raw_output)
151
+
152
+ # Handle both single results and arrays
153
+ results = tfsec_data.get("results", [])
154
+ if not isinstance(results, list):
155
+ results = [results] if results else []
156
+
157
+ for result in results:
158
+ severity = result.get("severity", "UNKNOWN").upper()
159
+
160
+ finding = TfSecFinding(
161
+ rule_id=result.get("rule_id", ""),
162
+ rule_description=result.get("rule_description", ""),
163
+ severity=severity,
164
+ resource=result.get("resource", ""),
165
+ location=result.get("location", {}),
166
+ description=result.get("description", "")
167
+ )
168
+ findings.append(finding)
169
+
170
+ # Count by severity
171
+ severity_key = severity.lower()
172
+ if severity_key in counts:
173
+ counts[severity_key] += 1
174
+
175
+ except json.JSONDecodeError:
176
+ # If JSON parsing fails, treat as text output with no findings
177
+ pass
178
+ except Exception as e:
179
+ # Log parsing error but continue
180
+ pass
181
+
182
+ return TfSecScanOutput(
183
+ scan_successful=scan_successful,
184
+ findings=findings,
185
+ total_findings=len(findings),
186
+ critical_count=counts["critical"],
187
+ high_count=counts["high"],
188
+ medium_count=counts["medium"],
189
+ low_count=counts["low"],
190
+ scan_duration=scan_duration,
191
+ raw_output=raw_output
192
+ )
193
+
194
+ def should_block_apply(self, scan_output: TfSecScanOutput, block_on_severity: List[str]) -> bool:
195
+ """
196
+ Determine if terraform apply should be blocked based on findings.
197
+
198
+ Args:
199
+ scan_output: Results from tfsec scan
200
+ block_on_severity: List of severity levels that should block apply
201
+
202
+ Returns:
203
+ bool: True if apply should be blocked
204
+ """
205
+ if not scan_output.scan_successful:
206
+ # Block on scan failures
207
+ return True
208
+
209
+ block_levels = [level.upper() for level in block_on_severity]
210
+
211
+ for finding in scan_output.findings:
212
+ if finding.severity.upper() in block_levels:
213
+ return True
214
+
215
+ return False
216
+
217
+ def create_findings_artifact(
218
+ self,
219
+ scan_output: TfSecScanOutput,
220
+ artifact_path: str
221
+ ) -> bool:
222
+ """
223
+ Create JSON artifact file with scan findings.
224
+
225
+ Args:
226
+ scan_output: Results from tfsec scan
227
+ artifact_path: Path where to save the artifact
228
+
229
+ Returns:
230
+ bool: True if artifact was created successfully
231
+ """
232
+ try:
233
+ # Ensure directory exists
234
+ os.makedirs(os.path.dirname(artifact_path), exist_ok=True)
235
+
236
+ # Create artifact data
237
+ artifact_data = {
238
+ "scan_timestamp": time.time(),
239
+ "scan_successful": scan_output.scan_successful,
240
+ "total_findings": scan_output.total_findings,
241
+ "severity_counts": {
242
+ "critical": scan_output.critical_count,
243
+ "high": scan_output.high_count,
244
+ "medium": scan_output.medium_count,
245
+ "low": scan_output.low_count
246
+ },
247
+ "scan_duration": scan_output.scan_duration,
248
+ "findings": [finding.model_dump() for finding in scan_output.findings],
249
+ "raw_output": scan_output.raw_output
250
+ }
251
+
252
+ # Write artifact file
253
+ with open(artifact_path, 'w') as f:
254
+ json.dump(artifact_data, f, indent=2)
255
+
256
+ return True
257
+
258
+ except Exception as e:
259
+ return False
@@ -0,0 +1,21 @@
1
+ """
2
+ Shell Agent - Multi-Agent Architecture
3
+
4
+ This agent specializes in secure shell command execution and serves as a shared service
5
+ for other agents (Git, Terraform, etc.) that need to execute shell commands.
6
+
7
+ Exports shell tools for now. Agent implementation coming later.
8
+ """
9
+
10
+ from diagram_to_iac.tools.shell import shell_exec, ShellExecutor
11
+ from .agent import ShellAgent, ShellAgentInput, ShellAgentOutput
12
+ from .detector import build_stack_histogram
13
+
14
+ __all__ = [
15
+ "shell_exec",
16
+ "ShellExecutor",
17
+ "ShellAgent",
18
+ "ShellAgentInput",
19
+ "ShellAgentOutput",
20
+ "build_stack_histogram",
21
+ ]
@@ -0,0 +1,122 @@
1
+ import uuid
2
+ import logging
3
+ from typing import TypedDict, Optional
4
+
5
+ from langgraph.graph import StateGraph, END
6
+ from langgraph.checkpoint.memory import MemorySaver
7
+ from langchain_core.messages import HumanMessage
8
+ from pydantic import BaseModel, Field
9
+
10
+ from diagram_to_iac.tools.shell import ShellExecInput, ShellExecutor
11
+ from diagram_to_iac.core.agent_base import AgentBase
12
+ from diagram_to_iac.core.memory import create_memory
13
+ from diagram_to_iac.services.observability import log_event
14
+
15
+
16
+ class ShellAgentInput(BaseModel):
17
+ command: str = Field(..., description="Shell command to execute")
18
+ cwd: Optional[str] = Field(None, description="Working directory")
19
+ timeout: Optional[int] = Field(None, description="Execution timeout")
20
+ thread_id: Optional[str] = Field(None, description="Optional thread id")
21
+
22
+
23
+ class ShellAgentOutput(BaseModel):
24
+ output: str = Field(..., description="Command output")
25
+ exit_code: int = Field(..., description="Exit code")
26
+ thread_id: str = Field(..., description="Thread id used")
27
+ error_message: Optional[str] = Field(None, description="Error message if failed")
28
+
29
+
30
+ class ShellAgentState(TypedDict):
31
+ input_message: HumanMessage
32
+ cwd: Optional[str]
33
+ timeout: Optional[int]
34
+ result: str
35
+ exit_code: int
36
+ error_message: Optional[str]
37
+
38
+
39
+ class ShellAgent(AgentBase):
40
+ """Minimal agent that executes a single shell command using ShellExecutor."""
41
+
42
+ def __init__(self, memory_type: str = "persistent"):
43
+ self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
44
+ if not logging.getLogger().hasHandlers():
45
+ logging.basicConfig(
46
+ level=logging.INFO,
47
+ format="%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s",
48
+ datefmt="%Y-%m-%d %H:%M:%S",
49
+ )
50
+
51
+ self.memory_type = memory_type
52
+ self.memory = create_memory(memory_type)
53
+ self.checkpointer = MemorySaver()
54
+ self.shell_executor = ShellExecutor(memory_type=memory_type)
55
+ self.runnable = self._build_graph()
56
+
57
+ def _shell_node(self, state: ShellAgentState) -> ShellAgentState:
58
+ shell_input = ShellExecInput(
59
+ command=state["input_message"].content,
60
+ cwd=state.get("cwd"),
61
+ timeout=state.get("timeout"),
62
+ )
63
+ try:
64
+ result = self.shell_executor.shell_exec(shell_input)
65
+ state["result"] = result.output
66
+ state["exit_code"] = result.exit_code
67
+ state["error_message"] = None
68
+ except Exception as e:
69
+ state["result"] = ""
70
+ state["exit_code"] = -1
71
+ state["error_message"] = str(e)
72
+ return state
73
+
74
+ def _build_graph(self):
75
+ graph_builder = StateGraph(ShellAgentState)
76
+ graph_builder.add_node("shell_node", self._shell_node)
77
+ graph_builder.set_entry_point("shell_node")
78
+ graph_builder.add_edge("shell_node", END)
79
+ return graph_builder.compile(checkpointer=self.checkpointer)
80
+
81
+ def run(self, agent_input: ShellAgentInput) -> ShellAgentOutput:
82
+ thread_id = agent_input.thread_id or str(uuid.uuid4())
83
+ log_event(
84
+ "shell_agent_run_start",
85
+ command=agent_input.command,
86
+ thread_id=thread_id,
87
+ )
88
+ initial_state: ShellAgentState = {
89
+ "input_message": HumanMessage(content=agent_input.command),
90
+ "cwd": agent_input.cwd,
91
+ "timeout": agent_input.timeout,
92
+ "result": "",
93
+ "exit_code": 0,
94
+ "error_message": None,
95
+ }
96
+
97
+ result_state = self.runnable.invoke(
98
+ initial_state, config={"configurable": {"thread_id": thread_id}}
99
+ )
100
+
101
+ output = ShellAgentOutput(
102
+ output=result_state.get("result", ""),
103
+ exit_code=result_state.get("exit_code", -1),
104
+ thread_id=thread_id,
105
+ error_message=result_state.get("error_message"),
106
+ )
107
+
108
+ log_event(
109
+ "shell_agent_run_end",
110
+ thread_id=thread_id,
111
+ exit_code=output.exit_code,
112
+ error=output.error_message,
113
+ )
114
+
115
+ return output
116
+
117
+ # AgentBase requirements
118
+ def plan(self, *args, **kwargs):
119
+ return {"description": "Execute shell command"}
120
+
121
+ def report(self, *args, **kwargs):
122
+ return {}
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import fnmatch
5
+ from typing import Dict
6
+
7
+ from .agent import ShellAgent, ShellAgentInput
8
+
9
+
10
+ def _count_files(pattern: str, repo_path: str, shell: ShellAgent) -> int:
11
+ """Count files matching pattern using shell find with Python fallback."""
12
+ try:
13
+ result = shell.run(
14
+ ShellAgentInput(
15
+ command=f"bash -c \"find . -name '{pattern}' -type f | wc -l\"",
16
+ cwd=repo_path,
17
+ )
18
+ )
19
+ if result.exit_code == 0:
20
+ return int(result.output.strip())
21
+ raise RuntimeError(result.error_message or "find failed")
22
+ except Exception:
23
+ count = 0
24
+ for root, _dirs, files in os.walk(repo_path):
25
+ count += len(fnmatch.filter(files, pattern))
26
+ return count
27
+
28
+
29
+ def build_stack_histogram(repo_path: str, shell: ShellAgent) -> Dict[str, float]:
30
+ """Build a normalized stack histogram for Terraform and shell files."""
31
+ tf_count = _count_files("*.tf", repo_path, shell)
32
+ sh_count = _count_files("*.sh", repo_path, shell)
33
+
34
+ # Apply weighting
35
+ tf_weight = tf_count * 2
36
+ if os.path.exists(os.path.join(repo_path, "main.tf")):
37
+ tf_weight += 3
38
+ sh_weight = sh_count
39
+
40
+ weights = {}
41
+ if tf_weight:
42
+ weights["terraform"] = tf_weight
43
+ if sh_weight:
44
+ weights["shell"] = sh_weight
45
+
46
+ total = sum(weights.values())
47
+ if total == 0:
48
+ return {}
49
+
50
+ return {k: v / total for k, v in weights.items()}
@@ -0,0 +1,17 @@
1
+ """Supervisor LangGraph Agent Package."""
2
+
3
+ from .agent import (
4
+ SupervisorAgent,
5
+ SupervisorAgentInput,
6
+ SupervisorAgentOutput,
7
+ )
8
+ from .demonstrator import DryRunDemonstrator
9
+ from .pat_loop import request_and_wait_for_pat
10
+
11
+ __all__ = [
12
+ "SupervisorAgent",
13
+ "SupervisorAgentInput",
14
+ "SupervisorAgentOutput",
15
+ "DryRunDemonstrator",
16
+ "request_and_wait_for_pat",
17
+ ]