pluto-ai 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 (34) hide show
  1. pluto/__init__.py +18 -0
  2. pluto/__pycache__/__init__.cpython-313.pyc +0 -0
  3. pluto/__pycache__/cli.cpython-313.pyc +0 -0
  4. pluto/analyzers/__init__.py +11 -0
  5. pluto/analyzers/__pycache__/__init__.cpython-313.pyc +0 -0
  6. pluto/analyzers/__pycache__/code_analyzer.cpython-313.pyc +0 -0
  7. pluto/analyzers/__pycache__/git_analyzer.cpython-313.pyc +0 -0
  8. pluto/analyzers/code_analyzer.py +34 -0
  9. pluto/analyzers/git_analyzer.py +15 -0
  10. pluto/cli.py +131 -0
  11. pluto/providers/__init__.py +12 -0
  12. pluto/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  13. pluto/providers/__pycache__/claude_provider.cpython-313.pyc +0 -0
  14. pluto/providers/__pycache__/ollama_provider.cpython-313.pyc +0 -0
  15. pluto/providers/__pycache__/openai_provider.cpython-313.pyc +0 -0
  16. pluto/providers/claude_provider.py +110 -0
  17. pluto/providers/ollama_provider.py +90 -0
  18. pluto/providers/openai_provider.py +95 -0
  19. pluto/reporters/__init__.py +18 -0
  20. pluto/reporters/__pycache__/__init__.cpython-313.pyc +0 -0
  21. pluto/reporters/__pycache__/json_reporter.cpython-313.pyc +0 -0
  22. pluto/reporters/__pycache__/markdown_reporter.cpython-313.pyc +0 -0
  23. pluto/reporters/__pycache__/pdf_reporter.cpython-313.pyc +0 -0
  24. pluto/reporters/__pycache__/terminal_reporter.cpython-313.pyc +0 -0
  25. pluto/reporters/json_reporter.py +21 -0
  26. pluto/reporters/markdown_reporter.py +34 -0
  27. pluto/reporters/pdf_reporter.py +140 -0
  28. pluto/reporters/terminal_reporter.py +47 -0
  29. pluto/utils/__init__.py +5 -0
  30. pluto_ai-1.0.0.dist-info/METADATA +241 -0
  31. pluto_ai-1.0.0.dist-info/RECORD +34 -0
  32. pluto_ai-1.0.0.dist-info/WHEEL +5 -0
  33. pluto_ai-1.0.0.dist-info/entry_points.txt +2 -0
  34. pluto_ai-1.0.0.dist-info/top_level.txt +1 -0
pluto/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ Pluto - AI-Powered Code Security Analyzer
3
+
4
+ A powerful CLI tool that uses AI to detect security vulnerabilities in your code.
5
+
6
+ Author: 0xSaikat
7
+ Website: https://hackbit.org
8
+ License: MIT
9
+ """
10
+
11
+ __version__ = "1.0.0"
12
+ __author__ = "0xSaikat"
13
+ __email__ = "contact@hackbit.org"
14
+ __license__ = "MIT"
15
+
16
+ from pluto.cli import cli
17
+
18
+ __all__ = ['cli']
Binary file
@@ -0,0 +1,11 @@
1
+ """
2
+ Code analysis modules for Pluto.
3
+
4
+ This package contains analyzers for different types of code inputs
5
+ including single files, directories, and git repositories.
6
+ """
7
+
8
+ from pluto.analyzers.code_analyzer import CodeAnalyzer
9
+ from pluto.analyzers.git_analyzer import GitAnalyzer
10
+
11
+ __all__ = ['CodeAnalyzer', 'GitAnalyzer']
@@ -0,0 +1,34 @@
1
+ from typing import List, Dict, Optional
2
+ import json
3
+
4
+ class CodeAnalyzer:
5
+ def __init__(self, provider: str = 'claude', model: str = 'claude-sonnet-4-20250514'):
6
+ self.provider = provider
7
+ self.model = model
8
+
9
+ if provider == 'claude':
10
+ from pluto.providers.claude_provider import ClaudeProvider
11
+ self.ai_provider = ClaudeProvider(model)
12
+ elif provider == 'openai':
13
+ from pluto.providers.openai_provider import OpenAIProvider
14
+ self.ai_provider = OpenAIProvider(model)
15
+ elif provider == 'ollama':
16
+ from pluto.providers.ollama_provider import OllamaProvider
17
+ self.ai_provider = OllamaProvider(model)
18
+ else:
19
+ raise ValueError(f"Unknown provider: {provider}")
20
+
21
+ def analyze_file(self, file_path: str) -> List[Dict]:
22
+ """Analyze a single file for vulnerabilities"""
23
+ try:
24
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
25
+ code = f.read()
26
+
27
+ if not code.strip():
28
+ return []
29
+
30
+ vulnerabilities = self.ai_provider.analyze_code(code, file_path)
31
+ return vulnerabilities
32
+ except Exception as e:
33
+ print(f"Error analyzing {file_path}: {str(e)}")
34
+ return []
@@ -0,0 +1,15 @@
1
+ import os
2
+ import tempfile
3
+ import shutil
4
+ from git import Repo
5
+
6
+ class GitAnalyzer:
7
+ def clone_repo(self, repo_url: str) -> str:
8
+ """Clone a git repository to a temporary directory"""
9
+ temp_dir = tempfile.mkdtemp(prefix='pluto_')
10
+ try:
11
+ Repo.clone_from(repo_url, temp_dir, depth=1)
12
+ return temp_dir
13
+ except Exception as e:
14
+ shutil.rmtree(temp_dir, ignore_errors=True)
15
+ raise Exception(f"Failed to clone repository: {str(e)}")
pluto/cli.py ADDED
@@ -0,0 +1,131 @@
1
+ import click
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional, List
6
+ import sys
7
+
8
+ def print_banner():
9
+ """Print the Pluto banner with styled text."""
10
+ banner = (
11
+ "\033[1;36m"
12
+ "\n"
13
+ "╭─────[By 0xSaikat]───────────────────────────────────╮\n"
14
+ "│ │\n"
15
+ "│ ____ __ __ │\n"
16
+ "│ / __ \\/ /_ __/ /_____ │\n"
17
+ "│ / /_/ / / / / / __/ __ \\ │\n"
18
+ "│ / ____/ / /_/ / /_/ /_/ / │\n"
19
+ "│ /_/ /_/\\__,_/\\__/\\____/ V-1.0 │\n"
20
+ "│ │\n"
21
+ "│ AI-Powered Code Security Analyzer │\n"
22
+ "│ │\n"
23
+ "╰─────────────────────────────────[hackbit.org]───────╯\n"
24
+ "\033[0m"
25
+ )
26
+ print(banner)
27
+
28
+ @click.group(invoke_without_command=True)
29
+ @click.pass_context
30
+ @click.version_option(version='1.0.0')
31
+ def cli(ctx):
32
+ """Pluto - AI-Powered Code Security Analyzer"""
33
+ if ctx.invoked_subcommand is None:
34
+ print_banner()
35
+ click.echo("\nUse 'pluto scan --help' to see available options\n")
36
+
37
+ @cli.command()
38
+ @click.option('-code', '--code-file', type=click.Path(exists=True), help='Analyze a single code file')
39
+ @click.option('-dir', '--directory', type=click.Path(exists=True), help='Analyze entire directory')
40
+ @click.option('-git', '--git-repo', type=str, help='Analyze GitHub repository')
41
+ @click.option('--provider', type=click.Choice(['claude', 'openai', 'ollama']), default='claude', help='AI provider')
42
+ @click.option('--model', type=str, default='claude-sonnet-4-20250514', help='Model name')
43
+ @click.option('--report', type=click.Choice(['terminal', 'pdf', 'json', 'html', 'markdown']), default='terminal', help='Report format')
44
+ @click.option('--output', type=str, default='pluto_report', help='Output file name (without extension)')
45
+ @click.option('--min-severity', type=click.Choice(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']), default='LOW', help='Minimum severity level')
46
+ def scan(code_file, directory, git_repo, provider, model, report, output, min_severity):
47
+ """Scan code for security vulnerabilities"""
48
+ print_banner()
49
+ from pluto.analyzers.code_analyzer import CodeAnalyzer
50
+ from pluto.reporters.terminal_reporter import TerminalReporter
51
+ from pluto.reporters.pdf_reporter import PDFReporter
52
+ from pluto.reporters.json_reporter import JSONReporter
53
+ from pluto.reporters.markdown_reporter import MarkdownReporter
54
+
55
+ analyzer = CodeAnalyzer(provider=provider, model=model)
56
+
57
+
58
+ files_to_analyze = []
59
+
60
+ if code_file:
61
+ files_to_analyze.append(code_file)
62
+ elif directory:
63
+ files_to_analyze = get_code_files(directory)
64
+ elif git_repo:
65
+ click.echo("Cloning repository...")
66
+ from pluto.analyzers.git_analyzer import GitAnalyzer
67
+ git_analyzer = GitAnalyzer()
68
+ repo_path = git_analyzer.clone_repo(git_repo)
69
+ files_to_analyze = get_code_files(repo_path)
70
+ else:
71
+ click.echo("Error: Please specify -code, -dir, or -git")
72
+ return
73
+
74
+ if not files_to_analyze:
75
+ click.echo("No code files found to analyze")
76
+ return
77
+
78
+ click.echo(f"Analyzing {len(files_to_analyze)} file(s)...")
79
+
80
+
81
+ all_results = []
82
+ for file_path in files_to_analyze:
83
+ click.echo(f"Scanning: {file_path}")
84
+ results = analyzer.analyze_file(file_path)
85
+ if results:
86
+ all_results.extend(results)
87
+
88
+
89
+ severity_order = {'LOW': 0, 'MEDIUM': 1, 'HIGH': 2, 'CRITICAL': 3}
90
+ min_level = severity_order[min_severity]
91
+ filtered_results = [r for r in all_results if severity_order.get(r.get('severity', 'LOW'), 0) >= min_level]
92
+
93
+ if report == 'terminal' or report == 'terminal':
94
+ reporter = TerminalReporter()
95
+ reporter.generate(filtered_results)
96
+
97
+ if report == 'pdf':
98
+ reporter = PDFReporter()
99
+ reporter.generate(filtered_results, f"{output}.pdf")
100
+ click.echo(f"\nPDF report saved to: {output}.pdf")
101
+
102
+ if report == 'json':
103
+ reporter = JSONReporter()
104
+ reporter.generate(filtered_results, f"{output}.json")
105
+ click.echo(f"\nJSON report saved to: {output}.json")
106
+
107
+ if report == 'markdown':
108
+ reporter = MarkdownReporter()
109
+ reporter.generate(filtered_results, f"{output}.md")
110
+ click.echo(f"\nMarkdown report saved to: {output}.md")
111
+
112
+ def get_code_files(path):
113
+ """Get all code files from a directory"""
114
+ code_extensions = {'.py', '.js', '.jsx', '.ts', '.tsx', '.java', '.cpp', '.c', '.go', '.rs', '.php', '.rb', '.swift', '.kt'}
115
+ files = []
116
+ path_obj = Path(path)
117
+
118
+ if path_obj.is_file():
119
+ return [str(path_obj)]
120
+
121
+ for file in path_obj.rglob('*'):
122
+ if file.is_file() and file.suffix in code_extensions:
123
+
124
+ if any(skip in file.parts for skip in ['node_modules', 'venv', '.git', '__pycache__', 'dist', 'build']):
125
+ continue
126
+ files.append(str(file))
127
+
128
+ return files
129
+
130
+ if __name__ == '__main__':
131
+ cli()
@@ -0,0 +1,12 @@
1
+ """
2
+ AI provider integrations for Pluto.
3
+
4
+ This package contains integrations with various AI providers
5
+ including Claude (Anthropic), OpenAI, and Ollama (local).
6
+ """
7
+
8
+ from pluto.providers.claude_provider import ClaudeProvider
9
+ from pluto.providers.openai_provider import OpenAIProvider
10
+ from pluto.providers.ollama_provider import OllamaProvider
11
+
12
+ __all__ = ['ClaudeProvider', 'OpenAIProvider', 'OllamaProvider']
@@ -0,0 +1,110 @@
1
+ import os
2
+ import json
3
+ from typing import List, Dict
4
+
5
+ class ClaudeProvider:
6
+ def __init__(self, model: str = 'claude-sonnet-4-20250514'):
7
+ self.model = model
8
+ try:
9
+ from anthropic import Anthropic
10
+ api_key = os.environ.get('ANTHROPIC_API_KEY')
11
+ if not api_key:
12
+ raise ValueError("ANTHROPIC_API_KEY environment variable not set")
13
+ self.client = Anthropic(api_key=api_key)
14
+ except ImportError:
15
+ raise ImportError("Please install: pip install anthropic")
16
+
17
+ def analyze_code(self, code: str, file_path: str) -> List[Dict]:
18
+ """Analyze code using Claude"""
19
+ prompt = self._create_prompt(code, file_path)
20
+
21
+ try:
22
+ response = self.client.messages.create(
23
+ model=self.model,
24
+ max_tokens=4096,
25
+ messages=[
26
+ {"role": "user", "content": prompt}
27
+ ]
28
+ )
29
+
30
+ result_text = response.content[0].text
31
+
32
+ vulnerabilities = self._parse_response(result_text)
33
+ return vulnerabilities
34
+ except Exception as e:
35
+ print(f"Error calling Claude API: {str(e)}")
36
+ return []
37
+
38
+ def _create_prompt(self, code: str, file_path: str) -> str:
39
+ return f"""Analyze the following code for security vulnerabilities. Focus on:
40
+
41
+ 1. SQL Injection
42
+ 2. XSS (Cross-Site Scripting)
43
+ 3. Authentication/Authorization issues
44
+ 4. Hardcoded secrets/credentials
45
+ 5. Insecure cryptography
46
+ 6. Path traversal
47
+ 7. Command injection
48
+ 8. CSRF vulnerabilities
49
+ 9. Insecure dependencies
50
+ 10. Buffer overflows
51
+ 11. Race conditions
52
+ 12. Logic flaws
53
+
54
+ File: {file_path}
55
+
56
+ Code:
57
+ ```
58
+ {code}
59
+ ```
60
+
61
+ For each vulnerability found, provide a JSON object with:
62
+ - severity: "CRITICAL", "HIGH", "MEDIUM", or "LOW"
63
+ - type: vulnerability type
64
+ - line: approximate line number (if identifiable)
65
+ - description: what's vulnerable
66
+ - impact: potential damage
67
+ - recommendation: how to fix
68
+
69
+ Return ONLY a JSON array of vulnerabilities. If no vulnerabilities found, return an empty array [].
70
+ Do not include any explanation, just the JSON array."""
71
+
72
+ def _parse_response(self, response: str) -> List[Dict]:
73
+ """Parse AI response to extract vulnerabilities"""
74
+ try:
75
+
76
+ start = response.find('[')
77
+ end = response.rfind(']') + 1
78
+
79
+ if start != -1 and end > start:
80
+ json_str = response[start:end]
81
+ vulnerabilities = json.loads(json_str)
82
+
83
+
84
+ for vuln in vulnerabilities:
85
+ severity = vuln.get('severity', 'LOW')
86
+
87
+ if '|' in severity:
88
+ levels = severity.split('|')
89
+ if 'CRITICAL' in levels:
90
+ vuln['severity'] = 'CRITICAL'
91
+ elif 'HIGH' in levels:
92
+ vuln['severity'] = 'HIGH'
93
+ elif 'MEDIUM' in levels:
94
+ vuln['severity'] = 'MEDIUM'
95
+ else:
96
+ vuln['severity'] = 'LOW'
97
+ else:
98
+
99
+ severity = severity.upper().strip()
100
+ if severity not in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
101
+ vuln['severity'] = 'MEDIUM'
102
+ else:
103
+ vuln['severity'] = severity
104
+
105
+ return vulnerabilities
106
+
107
+ return []
108
+ except json.JSONDecodeError:
109
+ print("Failed to parse JSON response")
110
+ return []
@@ -0,0 +1,90 @@
1
+ import json
2
+ from typing import List, Dict
3
+ import requests
4
+
5
+ class OllamaProvider:
6
+ def __init__(self, model: str = 'codellama'):
7
+ self.model = model
8
+ self.base_url = "http://localhost:11434"
9
+
10
+ def analyze_code(self, code: str, file_path: str) -> List[Dict]:
11
+ """Analyze code using Ollama"""
12
+ prompt = self._create_prompt(code, file_path)
13
+
14
+ try:
15
+ response = requests.post(
16
+ f"{self.base_url}/api/generate",
17
+ json={
18
+ "model": self.model,
19
+ "prompt": prompt,
20
+ "stream": False
21
+ },
22
+ timeout=120
23
+ )
24
+
25
+ if response.status_code == 200:
26
+ result = response.json()
27
+ result_text = result.get('response', '')
28
+ return self._parse_response(result_text)
29
+ else:
30
+ print(f"Ollama API error: {response.status_code}")
31
+ return []
32
+ except requests.exceptions.ConnectionError:
33
+ print("Error: Cannot connect to Ollama. Make sure Ollama is running (ollama serve)")
34
+ return []
35
+ except Exception as e:
36
+ print(f"Error calling Ollama: {str(e)}")
37
+ return []
38
+
39
+ def _create_prompt(self, code: str, file_path: str) -> str:
40
+ return f"""Analyze this code for security vulnerabilities.
41
+
42
+ File: {file_path}
43
+
44
+ Code:
45
+ ```
46
+ {code}
47
+ ```
48
+
49
+ Find security issues like: SQL injection, XSS, hardcoded secrets, insecure crypto, command injection, etc.
50
+
51
+ Return ONLY a JSON array. Format:
52
+ [{{"severity": "CRITICAL|HIGH|MEDIUM|LOW", "type": "issue type", "line": number, "description": "what's wrong", "impact": "potential damage", "recommendation": "how to fix"}}]
53
+
54
+ If no issues, return []. JSON only, no explanation."""
55
+
56
+ def _parse_response(self, response: str) -> List[Dict]:
57
+ """Parse Ollama response"""
58
+ try:
59
+ start = response.find('[')
60
+ end = response.rfind(']') + 1
61
+
62
+ if start != -1 and end > start:
63
+ json_str = response[start:end]
64
+ vulnerabilities = json.loads(json_str)
65
+
66
+ for vuln in vulnerabilities:
67
+ severity = vuln.get('severity', 'LOW')
68
+
69
+ if '|' in severity:
70
+ levels = severity.split('|')
71
+ if 'CRITICAL' in levels:
72
+ vuln['severity'] = 'CRITICAL'
73
+ elif 'HIGH' in levels:
74
+ vuln['severity'] = 'HIGH'
75
+ elif 'MEDIUM' in levels:
76
+ vuln['severity'] = 'MEDIUM'
77
+ else:
78
+ vuln['severity'] = 'LOW'
79
+ else:
80
+
81
+ severity = severity.upper().strip()
82
+ if severity not in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
83
+ vuln['severity'] = 'MEDIUM'
84
+ else:
85
+ vuln['severity'] = severity
86
+
87
+ return vulnerabilities
88
+ return []
89
+ except json.JSONDecodeError:
90
+ return []
@@ -0,0 +1,95 @@
1
+ import os
2
+ import json
3
+ from typing import List, Dict
4
+
5
+ class OpenAIProvider:
6
+ def __init__(self, model: str = 'gpt-4'):
7
+ self.model = model
8
+ try:
9
+ from openai import OpenAI
10
+ api_key = os.environ.get('OPENAI_API_KEY')
11
+ if not api_key:
12
+ raise ValueError("OPENAI_API_KEY environment variable not set")
13
+ self.client = OpenAI(api_key=api_key)
14
+ except ImportError:
15
+ raise ImportError("Please install: pip install openai")
16
+
17
+ def analyze_code(self, code: str, file_path: str) -> List[Dict]:
18
+ """Analyze code using OpenAI"""
19
+ prompt = self._create_prompt(code, file_path)
20
+
21
+ try:
22
+ response = self.client.chat.completions.create(
23
+ model=self.model,
24
+ messages=[
25
+ {"role": "system", "content": "You are a security expert analyzing code for vulnerabilities."},
26
+ {"role": "user", "content": prompt}
27
+ ],
28
+ temperature=0.3
29
+ )
30
+
31
+ result_text = response.choices[0].message.content
32
+ vulnerabilities = self._parse_response(result_text)
33
+ return vulnerabilities
34
+ except Exception as e:
35
+ print(f"Error calling OpenAI API: {str(e)}")
36
+ return []
37
+
38
+ def _create_prompt(self, code: str, file_path: str) -> str:
39
+ return f"""Analyze the following code for security vulnerabilities.
40
+
41
+ File: {file_path}
42
+
43
+ Code:
44
+ ```
45
+ {code}
46
+ ```
47
+
48
+ Return ONLY a JSON array of vulnerabilities. Format:
49
+ [
50
+ {{
51
+ "severity": "CRITICAL|HIGH|MEDIUM|LOW",
52
+ "type": "vulnerability type",
53
+ "line": line_number,
54
+ "description": "description",
55
+ "impact": "impact",
56
+ "recommendation": "fix"
57
+ }}
58
+ ]
59
+
60
+ If no vulnerabilities, return []."""
61
+
62
+ def _parse_response(self, response: str) -> List[Dict]:
63
+ """Parse AI response to extract vulnerabilities"""
64
+ try:
65
+ start = response.find('[')
66
+ end = response.rfind(']') + 1
67
+
68
+ if start != -1 and end > start:
69
+ json_str = response[start:end]
70
+ vulnerabilities = json.loads(json_str)
71
+
72
+ for vuln in vulnerabilities:
73
+ severity = vuln.get('severity', 'LOW')
74
+
75
+ if '|' in severity:
76
+ levels = severity.split('|')
77
+ if 'CRITICAL' in levels:
78
+ vuln['severity'] = 'CRITICAL'
79
+ elif 'HIGH' in levels:
80
+ vuln['severity'] = 'HIGH'
81
+ elif 'MEDIUM' in levels:
82
+ vuln['severity'] = 'MEDIUM'
83
+ else:
84
+ vuln['severity'] = 'LOW'
85
+ else:
86
+ severity = severity.upper().strip()
87
+ if severity not in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
88
+ vuln['severity'] = 'MEDIUM'
89
+ else:
90
+ vuln['severity'] = severity
91
+
92
+ return vulnerabilities
93
+ return []
94
+ except json.JSONDecodeError:
95
+ return []
@@ -0,0 +1,18 @@
1
+ """
2
+ Report generation modules for Pluto.
3
+
4
+ This package contains different report formats including
5
+ terminal, PDF, JSON, and Markdown outputs.
6
+ """
7
+
8
+ from pluto.reporters.terminal_reporter import TerminalReporter
9
+ from pluto.reporters.pdf_reporter import PDFReporter
10
+ from pluto.reporters.json_reporter import JSONReporter
11
+ from pluto.reporters.markdown_reporter import MarkdownReporter
12
+
13
+ __all__ = [
14
+ 'TerminalReporter',
15
+ 'PDFReporter',
16
+ 'JSONReporter',
17
+ 'MarkdownReporter'
18
+ ]
@@ -0,0 +1,21 @@
1
+ import json
2
+ from typing import List, Dict
3
+
4
+ class JSONReporter:
5
+ def generate(self, vulnerabilities: List[Dict], output_file: str):
6
+ """Generate JSON report"""
7
+ report = {
8
+ "total_vulnerabilities": len(vulnerabilities),
9
+ "severity_summary": self._get_severity_summary(vulnerabilities),
10
+ "vulnerabilities": vulnerabilities
11
+ }
12
+
13
+ with open(output_file, 'w') as f:
14
+ json.dump(report, f, indent=2)
15
+
16
+ def _get_severity_summary(self, vulnerabilities: List[Dict]) -> Dict:
17
+ summary = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
18
+ for vuln in vulnerabilities:
19
+ severity = vuln.get('severity', 'LOW')
20
+ summary[severity] = summary.get(severity, 0) + 1
21
+ return summary
@@ -0,0 +1,34 @@
1
+ from typing import List, Dict
2
+ from datetime import datetime
3
+
4
+ class MarkdownReporter:
5
+ def generate(self, vulnerabilities: List[Dict], output_file: str):
6
+ """Generate Markdown report"""
7
+ lines = []
8
+ lines.append("# Pluto Security Scan Report\n")
9
+ lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
10
+ lines.append(f"**Total Vulnerabilities:** {len(vulnerabilities)}\n")
11
+
12
+ severity_count = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
13
+ for vuln in vulnerabilities:
14
+ severity = vuln.get('severity', 'LOW')
15
+ severity_count[severity] = severity_count.get(severity, 0) + 1
16
+
17
+ lines.append("\n## Severity Summary\n")
18
+ lines.append("| Severity | Count |\n")
19
+ lines.append("|----------|-------|\n")
20
+ for severity, count in severity_count.items():
21
+ lines.append(f"| {severity} | {count} |\n")
22
+
23
+ lines.append("\n## Detailed Findings\n")
24
+ for i, vuln in enumerate(vulnerabilities, 1):
25
+ lines.append(f"\n### [{i}] {vuln.get('severity', 'LOW')} - {vuln.get('type', 'Unknown')}\n")
26
+ lines.append(f"**Description:** {vuln.get('description', 'N/A')}\n\n")
27
+ lines.append(f"**Impact:** {vuln.get('impact', 'N/A')}\n\n")
28
+ lines.append(f"**Recommendation:** {vuln.get('recommendation', 'N/A')}\n")
29
+ if vuln.get('line'):
30
+ lines.append(f"**Line:** {vuln['line']}\n")
31
+ lines.append("\n---\n")
32
+
33
+ with open(output_file, 'w') as f:
34
+ f.writelines(lines)
@@ -0,0 +1,140 @@
1
+ from typing import List, Dict
2
+ from datetime import datetime
3
+ import os
4
+
5
+ class PDFReporter:
6
+ def generate(self, vulnerabilities: List[Dict], output_file: str):
7
+ """Generate PDF report with logo"""
8
+ try:
9
+ from reportlab.lib.pagesizes import letter
10
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
11
+ from reportlab.lib.units import inch
12
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
13
+ from reportlab.lib import colors
14
+ from reportlab.lib.enums import TA_CENTER
15
+ except ImportError:
16
+ print("Error: reportlab not installed. Install with: pip install reportlab")
17
+ return
18
+
19
+ doc = SimpleDocTemplate(output_file, pagesize=letter)
20
+ styles = getSampleStyleSheet()
21
+ story = []
22
+
23
+ logo_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'logo.png')
24
+ if os.path.exists(logo_path):
25
+ try:
26
+ logo = Image(logo_path, width=2*inch, height=2*inch)
27
+ logo.hAlign = 'CENTER'
28
+ story.append(logo)
29
+ story.append(Spacer(1, 0.3*inch))
30
+ except:
31
+ pass
32
+
33
+ title_style = ParagraphStyle(
34
+ 'CustomTitle',
35
+ parent=styles['Heading1'],
36
+ fontSize=28,
37
+ textColor=colors.HexColor('#00CED1'),
38
+ spaceAfter=10,
39
+ alignment=TA_CENTER,
40
+ fontName='Helvetica-Bold'
41
+ )
42
+ story.append(Paragraph("🛡️ PLUTO SECURITY REPORT 🛡️", title_style))
43
+
44
+ subtitle_style = ParagraphStyle(
45
+ 'Subtitle',
46
+ parent=styles['Normal'],
47
+ fontSize=12,
48
+ textColor=colors.HexColor('#666666'),
49
+ alignment=TA_CENTER,
50
+ spaceAfter=20,
51
+ )
52
+ story.append(Paragraph("AI-Powered Code Security Analyzer", subtitle_style))
53
+ story.append(Paragraph("by 0xSaikat | hackbit.org", subtitle_style))
54
+ story.append(Spacer(1, 0.3*inch))
55
+
56
+ story.append(Paragraph(f"<b>Generated:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", styles['Normal']))
57
+ story.append(Paragraph(f"<b>Total Vulnerabilities Found:</b> {len(vulnerabilities)}", styles['Normal']))
58
+ story.append(Spacer(1, 0.3*inch))
59
+
60
+ severity_count = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
61
+ for vuln in vulnerabilities:
62
+ severity = vuln.get('severity', 'LOW')
63
+ severity_count[severity] = severity_count.get(severity, 0) + 1
64
+
65
+ data = [['Severity Level', 'Count', 'Priority']]
66
+ for severity in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
67
+ count = severity_count[severity]
68
+ priority = '🔴' if severity == 'CRITICAL' else '🟠' if severity == 'HIGH' else '🟡' if severity == 'MEDIUM' else '🟢'
69
+ data.append([severity, str(count), priority])
70
+
71
+ table = Table(data, colWidths=[2*inch, 1.5*inch, 1.5*inch])
72
+ table.setStyle(TableStyle([
73
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#00CED1')),
74
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
75
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
76
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
77
+ ('FONTSIZE', (0, 0), (-1, 0), 14),
78
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
79
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
80
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
81
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
82
+ ]))
83
+ story.append(table)
84
+ story.append(Spacer(1, 0.5*inch))
85
+
86
+ vuln_header_style = ParagraphStyle(
87
+ 'VulnHeader',
88
+ parent=styles['Heading2'],
89
+ fontSize=18,
90
+ textColor=colors.HexColor('#00CED1'),
91
+ spaceAfter=20,
92
+ )
93
+ story.append(Paragraph("📋 Detailed Vulnerability Findings", vuln_header_style))
94
+ story.append(Spacer(1, 0.2*inch))
95
+
96
+ if not vulnerabilities:
97
+ story.append(Paragraph("✅ <b>No vulnerabilities detected!</b>", styles['Normal']))
98
+ else:
99
+ severity_colors = {
100
+ 'CRITICAL': colors.HexColor('#FF0000'),
101
+ 'HIGH': colors.HexColor('#FF6600'),
102
+ 'MEDIUM': colors.HexColor('#FFA500'),
103
+ 'LOW': colors.HexColor('#00AA00')
104
+ }
105
+
106
+ for i, vuln in enumerate(vulnerabilities, 1):
107
+ severity = vuln.get('severity', 'LOW')
108
+ severity_color = severity_colors.get(severity, colors.black)
109
+
110
+ vuln_title_style = ParagraphStyle(
111
+ 'VulnTitle',
112
+ parent=styles['Heading3'],
113
+ fontSize=14,
114
+ textColor=severity_color,
115
+ spaceAfter=10,
116
+ )
117
+
118
+ story.append(Paragraph(f"<b>[{i}] {severity} - {vuln.get('type', 'Unknown')}</b>", vuln_title_style))
119
+
120
+ if vuln.get('line'):
121
+ story.append(Paragraph(f"<b>📍 Location:</b> Line {vuln['line']}", styles['Normal']))
122
+
123
+ story.append(Paragraph(f"<b>📝 Description:</b> {vuln.get('description', 'N/A')}", styles['Normal']))
124
+ story.append(Paragraph(f"<b>💥 Impact:</b> {vuln.get('impact', 'N/A')}", styles['Normal']))
125
+ story.append(Paragraph(f"<b>✅ Recommendation:</b> {vuln.get('recommendation', 'N/A')}", styles['Normal']))
126
+ story.append(Spacer(1, 0.3*inch))
127
+
128
+ story.append(Spacer(1, 0.5*inch))
129
+ footer_style = ParagraphStyle(
130
+ 'Footer',
131
+ parent=styles['Normal'],
132
+ fontSize=10,
133
+ textColor=colors.HexColor('#666666'),
134
+ alignment=TA_CENTER,
135
+ )
136
+ story.append(Paragraph("─" * 80, footer_style))
137
+ story.append(Paragraph("Generated by Pluto v1.0 | hackbit.org", footer_style))
138
+ story.append(Paragraph("AI-Powered Security Analysis Tool", footer_style))
139
+
140
+ doc.build(story)
@@ -0,0 +1,47 @@
1
+ from typing import List, Dict
2
+
3
+ class TerminalReporter:
4
+ COLORS = {
5
+ 'CRITICAL': '\033[91m',
6
+ 'HIGH': '\033[93m',
7
+ 'MEDIUM': '\033[94m',
8
+ 'LOW': '\033[92m',
9
+ 'RESET': '\033[0m',
10
+ 'BOLD': '\033[1m'
11
+ }
12
+
13
+ def generate(self, vulnerabilities: List[Dict]):
14
+ """Generate terminal report"""
15
+ if not vulnerabilities:
16
+ print(f"\n{self.COLORS['BOLD']}✓ No vulnerabilities found!{self.COLORS['RESET']}\n")
17
+ return
18
+
19
+ print(f"\n{self.COLORS['BOLD']}{'='*80}{self.COLORS['RESET']}")
20
+ print(f"{self.COLORS['BOLD']}PLUTO SECURITY SCAN REPORT{self.COLORS['RESET']}")
21
+ print(f"{self.COLORS['BOLD']}{'='*80}{self.COLORS['RESET']}\n")
22
+
23
+ severity_count = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
24
+ for vuln in vulnerabilities:
25
+ severity = vuln.get('severity', 'LOW')
26
+ severity_count[severity] = severity_count.get(severity, 0) + 1
27
+
28
+ print(f"Total Issues: {len(vulnerabilities)}")
29
+ print(f" {self.COLORS['CRITICAL']}● CRITICAL: {severity_count['CRITICAL']}{self.COLORS['RESET']}")
30
+ print(f" {self.COLORS['HIGH']}● HIGH: {severity_count['HIGH']}{self.COLORS['RESET']}")
31
+ print(f" {self.COLORS['MEDIUM']}● MEDIUM: {severity_count['MEDIUM']}{self.COLORS['RESET']}")
32
+ print(f" {self.COLORS['LOW']}● LOW: {severity_count['LOW']}{self.COLORS['RESET']}\n")
33
+
34
+ for i, vuln in enumerate(vulnerabilities, 1):
35
+ severity = vuln.get('severity', 'LOW')
36
+ color = self.COLORS.get(severity, self.COLORS['RESET'])
37
+
38
+ print(f"{color}{self.COLORS['BOLD']}[{i}] {severity}{self.COLORS['RESET']}")
39
+ print(f"Type: {vuln.get('type', 'Unknown')}")
40
+
41
+ if vuln.get('line'):
42
+ print(f"Line: {vuln['line']}")
43
+
44
+ print(f"Description: {vuln.get('description', 'N/A')}")
45
+ print(f"Impact: {vuln.get('impact', 'N/A')}")
46
+ print(f"Recommendation: {vuln.get('recommendation', 'N/A')}")
47
+ print(f"{'-'*80}\n")
@@ -0,0 +1,5 @@
1
+ """
2
+ Utility functions for Pluto.
3
+ """
4
+
5
+ __all__ = []
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: pluto-ai
3
+ Version: 1.0.0
4
+ Summary: AI-powered code security vulnerability scanner
5
+ Home-page: https://github.com/0xsaikat/pluto
6
+ Author: 0xSaikat
7
+ Author-email: 0xSaikat <contact@hackbit.org>
8
+ License: MIT
9
+ Project-URL: Homepage, https://hackbit.org
10
+ Project-URL: Repository, https://github.com/0xsaikat/pluto
11
+ Project-URL: Issues, https://github.com/0xsaikat/pluto/issues
12
+ Keywords: security,vulnerability,scanner,code-analysis,ai,static-analysis
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Security
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Requires-Python: >=3.7
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: click>=8.0.0
26
+ Requires-Dist: anthropic>=0.18.0
27
+ Requires-Dist: openai>=1.0.0
28
+ Requires-Dist: requests>=2.28.0
29
+ Requires-Dist: GitPython>=3.1.0
30
+ Requires-Dist: reportlab>=4.0.0
31
+ Dynamic: author
32
+ Dynamic: home-page
33
+ Dynamic: requires-python
34
+
35
+ # 🛡️ Pluto - AI-Powered Code Security Analyzer
36
+
37
+ <div align="center">
38
+
39
+ ```
40
+ ╭─────[By 0xSaikat]───────────────────────────────────╮
41
+ │ │
42
+ │ ____ __ __ │
43
+ │ / __ \/ /_ __/ /_____ │
44
+ │ / /_/ / / / / / __/ __ \ │
45
+ │ / ____/ / /_/ / /_/ /_/ / │
46
+ │ /_/ /_/\__,_/\__/\____/ V-1.0 │
47
+ │ │
48
+ │ AI-Powered Code Security Analyzer │
49
+ │ │
50
+ ╰─────────────────────────────────[hackbit.org]───────╯
51
+ ```
52
+
53
+ [![PyPI version](https://badge.fury.io/py/pluto-security-scanner.svg)](https://badge.fury.io/py/pluto-security-scanner)
54
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
55
+ [![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)
56
+
57
+ **Pluto** is a powerful CLI tool that uses AI to detect security vulnerabilities in your code.
58
+
59
+ [Features](#-features) • [Installation](#-installation) • [Usage](#-usage) • [Examples](#-examples) • [Contributing](#-contributing)
60
+
61
+ </div>
62
+
63
+ ---
64
+
65
+ ## 🚀 Features
66
+
67
+ - 🤖 **Multiple AI Providers**: Claude, OpenAI, Ollama (local)
68
+ - 📁 **Flexible Input**: Analyze files, directories, or GitHub repositories
69
+ - 📊 **Multiple Report Formats**: Terminal, PDF, JSON, Markdown
70
+ - 🔒 **Privacy-First**: Local analysis with Ollama support
71
+ - 🎯 **Severity Filtering**: Focus on CRITICAL, HIGH, MEDIUM, or LOW issues
72
+ - 🌐 **Multi-Language Support**: Python, JavaScript, Java, C/C++, Go, Rust, PHP, Ruby, and more
73
+
74
+ ## 🔍 Security Checks
75
+
76
+ Pluto detects:
77
+ - SQL Injection
78
+ - XSS (Cross-Site Scripting)
79
+ - Authentication/Authorization flaws
80
+ - Hardcoded secrets & credentials
81
+ - Insecure cryptography
82
+ - Path traversal
83
+ - Command injection
84
+ - CSRF vulnerabilities
85
+ - Insecure dependencies
86
+ - And many more...
87
+
88
+ ## 📦 Installation
89
+
90
+ ### From PyPI (Recommended)
91
+ ```bash
92
+ pip install pluto-ai
93
+ ```
94
+
95
+ ### From Source
96
+ ```bash
97
+ git clone https://github.com/0xsaikat/pluto.git
98
+ cd pluto
99
+ pip install -e .
100
+ ```
101
+
102
+ ## ⚙️ Setup
103
+
104
+ ### For Claude (Recommended)
105
+ ```bash
106
+ export ANTHROPIC_API_KEY='your-api-key-here'
107
+ ```
108
+ Get your API key from: https://console.anthropic.com/
109
+
110
+ ### For OpenAI
111
+ ```bash
112
+ export OPENAI_API_KEY='your-api-key-here'
113
+ ```
114
+
115
+ ### For Ollama (Local, Free)
116
+ ```bash
117
+ # Install Ollama from https://ollama.ai
118
+ ollama pull phi
119
+ ollama serve
120
+ ```
121
+
122
+ ## 💻 Usage
123
+
124
+ ### Basic Commands
125
+
126
+ ```bash
127
+ # Analyze a single file
128
+ pluto scan -code app.py
129
+
130
+ # Analyze entire directory
131
+ pluto scan -dir ./src --report pdf --output security_report
132
+
133
+ # Analyze GitHub repository
134
+ pluto scan -git https://github.com/user/repo --provider claude
135
+
136
+ # Use local AI (Ollama)
137
+ pluto scan -code app.py --provider ollama --model phi
138
+
139
+ # Filter by severity
140
+ pluto scan -dir ./src --min-severity HIGH
141
+ ```
142
+
143
+ ### Command Options
144
+
145
+ ```
146
+ Options:
147
+ -code, --code-file PATH Analyze a single code file
148
+ -dir, --directory PATH Analyze entire directory
149
+ -git, --git-repo TEXT Analyze GitHub repository
150
+ --provider [claude|openai|ollama] AI provider (default: claude)
151
+ --model TEXT Model name
152
+ --report [terminal|pdf|json|markdown] Report format (default: terminal)
153
+ --output TEXT Output file name
154
+ --min-severity [LOW|MEDIUM|HIGH|CRITICAL] Minimum severity level
155
+ --help Show this message and exit
156
+ ```
157
+
158
+ ## 📚 Examples
159
+
160
+ ### Quick Security Scan
161
+ ```bash
162
+ pluto scan -code myapp.py
163
+ ```
164
+
165
+ ### Full Project Audit
166
+ ```bash
167
+ pluto scan -dir ./backend --provider claude --report pdf --output project_audit
168
+ ```
169
+
170
+ ### GitHub Repository Analysis
171
+ ```bash
172
+ pluto scan -git https://github.com/user/vulnerable-app --report json
173
+ ```
174
+
175
+ ### Local Private Scan
176
+ ```bash
177
+ pluto scan -code sensitive_code.py --provider ollama --model phi
178
+ ```
179
+
180
+ ### CI/CD Integration
181
+ ```bash
182
+ pluto scan -dir ./src --report json --output results.json --min-severity HIGH
183
+ ```
184
+
185
+ ## 📊 Report Formats
186
+
187
+ - **Terminal**: Colorful, real-time output with severity highlighting
188
+ - **PDF**: Professional report with logo, charts, and detailed findings
189
+ - **JSON**: Machine-readable format for automation and CI/CD
190
+ - **Markdown**: Documentation-friendly format
191
+
192
+ ## 🎨 Supported Languages
193
+
194
+ Python • JavaScript • TypeScript • Java • C/C++ • Go • Rust • PHP • Ruby • Swift • Kotlin
195
+
196
+ ## 🔧 Configuration
197
+
198
+ Create a `.plutorc` file in your project root:
199
+
200
+ ```yaml
201
+ provider: claude
202
+ model: claude-sonnet-4-20250514
203
+ min_severity: MEDIUM
204
+ report_format: pdf
205
+ output_dir: ./security-reports
206
+ ```
207
+
208
+ ## 🤝 Contributing
209
+
210
+ Contributions are welcome! Please feel free to submit a Pull Request.
211
+
212
+ 1. Fork the repository
213
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
214
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
215
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
216
+ 5. Open a Pull Request
217
+
218
+ ## 📝 License
219
+
220
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
221
+
222
+ ## 👨‍💻 Author
223
+
224
+ **0xSaikat**
225
+ - Website: [hackbit.org](https://hackbit.org)
226
+ - GitHub: [@0xsaikat](https://github.com/0xsaikat)
227
+
228
+ ## 🙏 Acknowledgments
229
+
230
+ - Powered by Claude (Anthropic), OpenAI, and Ollama
231
+ - Built with ❤️ for the security community
232
+
233
+ ## ⚠️ Disclaimer
234
+
235
+ Pluto is a security analysis tool intended for educational and legitimate security testing purposes only. Always ensure you have permission before scanning code or repositories you don't own.
236
+
237
+ ---
238
+
239
+ <div align="center">
240
+ Made with 🛡️ by 0xSaikat | <a href="https://hackbit.org">hackbit.org</a>
241
+ </div>
@@ -0,0 +1,34 @@
1
+ pluto/__init__.py,sha256=TTmJeJ0nFpVYXyf6zkCACzeO28mscePECmQ84e8TBh0,339
2
+ pluto/cli.py,sha256=PCCfo_nxkbc6vEj_KHKHcfM0q3xIlKVOBNIVRWLppxI,5483
3
+ pluto/__pycache__/__init__.cpython-313.pyc,sha256=0tAqBWYvu5Bb_m-W75EJUlGYb0HrDYsS-S5iLp300Xg,549
4
+ pluto/__pycache__/cli.cpython-313.pyc,sha256=u9M-ZRLsHNqWWVuHCySrZSxKucA9kK2n8CAwGlvDbKc,7389
5
+ pluto/analyzers/__init__.py,sha256=pmycDq41M7GTCu4dZakpHWQ1fZyGs5wyuGsHOGmFzB4,319
6
+ pluto/analyzers/code_analyzer.py,sha256=jGD5DxOQH8q6mJPAsGL745ernyHptVihStBWqwudghw,1336
7
+ pluto/analyzers/git_analyzer.py,sha256=v-PfTC1tXyKzI_ZAi4AsaMOZWel-U_BefhyQIXsvd_U,497
8
+ pluto/analyzers/__pycache__/__init__.cpython-313.pyc,sha256=weev9qMQwN2igv0qNQxgWMdEr4aWQNrv-kASGdalObk,516
9
+ pluto/analyzers/__pycache__/code_analyzer.cpython-313.pyc,sha256=2sxPOFfMX7Mbz4_Wm95v5z-uI7VTqn5G_nJWR0FVP5A,2202
10
+ pluto/analyzers/__pycache__/git_analyzer.cpython-313.pyc,sha256=46TH3OpjeuvCFtdlGY6jh4xOFuUcIs6zthx4b6mr6MQ,1199
11
+ pluto/providers/__init__.py,sha256=69FM9e28yG9WY2L1_nlL3_ZUcObaSqMqm_NaeR4oSZI,407
12
+ pluto/providers/claude_provider.py,sha256=1JN62lcJ5ZNpbczK4dihAvJTlHlaMsCEVJ3FmHOEvVs,3729
13
+ pluto/providers/ollama_provider.py,sha256=8-7s1OZ41HbOYViROqBlULPXEOMq0lrjmfW-HbeA2Zs,3271
14
+ pluto/providers/openai_provider.py,sha256=2MtqF47W7EctCsnDu8ZUpbXLj0geMLau9nUW52M9i7c,3276
15
+ pluto/providers/__pycache__/__init__.cpython-313.pyc,sha256=f385h9iX_ISfx658L2toU2vo6jVDXTnwmOHkxBaWXQI,595
16
+ pluto/providers/__pycache__/claude_provider.cpython-313.pyc,sha256=IMGMh-V0jOmA7g_ZBH-QnRgZxBGbFB3NUmlsjtRqAqA,4531
17
+ pluto/providers/__pycache__/ollama_provider.cpython-313.pyc,sha256=4v0BEYGEHIY1qn4tyFOq-9X3uJh0dJ3AEPedVpmqAw8,4022
18
+ pluto/providers/__pycache__/openai_provider.cpython-313.pyc,sha256=_ks1GUR6AVNzN1uCz5gkNLW92ZfQH8bBR2z3324pEiU,4155
19
+ pluto/reporters/__init__.py,sha256=6Dfa9zmPtxIPZ1NTnXeJHUrS7Jt_A3DcDZpEwTQETLU,482
20
+ pluto/reporters/json_reporter.py,sha256=dbFiKHAnsLqa6Q5fssBdtCHijlpkqI7ggbZFB68LfzE,794
21
+ pluto/reporters/markdown_reporter.py,sha256=yjCQMuUYN_z0ZEndBGSETOyxlG41UsxvsydRYruR-Ks,1616
22
+ pluto/reporters/pdf_reporter.py,sha256=y3fYRxLsTzWJ2yLZtl_m2JMqKdq9q3J3hoVkravLzW8,6247
23
+ pluto/reporters/terminal_reporter.py,sha256=YX-ns_2-jWgnLiX4PHTBsltNKYC9NB7a9uJy-QQX0xc,2154
24
+ pluto/reporters/__pycache__/__init__.cpython-313.pyc,sha256=GG8hvKPhcBC1CRhuJBJXEKc9296KRp3gjEAy5I6NGqc,653
25
+ pluto/reporters/__pycache__/json_reporter.cpython-313.pyc,sha256=vl0K8BDFDx5oc4Ua61wUgLuqa9dS1jhmtTB_tw20OQA,1537
26
+ pluto/reporters/__pycache__/markdown_reporter.cpython-313.pyc,sha256=X0kBZNxyoyh-EVfOhrlYEW8pz5OzsabvBTFCEKyf8VI,3002
27
+ pluto/reporters/__pycache__/pdf_reporter.cpython-313.pyc,sha256=hM0jhelWsqIRMZSc-f2g5Xtbj9b6uGhEqwpujZW0JAw,7906
28
+ pluto/reporters/__pycache__/terminal_reporter.cpython-313.pyc,sha256=wug8LzBnIfu1c7gtAEZwyFvdrbRYBEEnP1nyhtXbmis,3669
29
+ pluto/utils/__init__.py,sha256=5WlXW0QLNc2t2UyqFBHOkAfNh4lUrJmAK7GbzDqjhow,50
30
+ pluto_ai-1.0.0.dist-info/METADATA,sha256=TBu4UneeiVOR6bVxkzSH5yboZbEA_rpkYMWeP1iIl2s,7195
31
+ pluto_ai-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ pluto_ai-1.0.0.dist-info/entry_points.txt,sha256=ridTJ852BSoC8KA1DVhgyn0mYFMniNINKwr5am5INLM,40
33
+ pluto_ai-1.0.0.dist-info/top_level.txt,sha256=KsGEOpwgREN1s1vqrZwUQCHhyc3oJ18-ldioP1XCjdA,6
34
+ pluto_ai-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pluto = pluto.cli:cli
@@ -0,0 +1 @@
1
+ pluto