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.
- pluto/__init__.py +18 -0
- pluto/__pycache__/__init__.cpython-313.pyc +0 -0
- pluto/__pycache__/cli.cpython-313.pyc +0 -0
- pluto/analyzers/__init__.py +11 -0
- pluto/analyzers/__pycache__/__init__.cpython-313.pyc +0 -0
- pluto/analyzers/__pycache__/code_analyzer.cpython-313.pyc +0 -0
- pluto/analyzers/__pycache__/git_analyzer.cpython-313.pyc +0 -0
- pluto/analyzers/code_analyzer.py +34 -0
- pluto/analyzers/git_analyzer.py +15 -0
- pluto/cli.py +131 -0
- pluto/providers/__init__.py +12 -0
- pluto/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- pluto/providers/__pycache__/claude_provider.cpython-313.pyc +0 -0
- pluto/providers/__pycache__/ollama_provider.cpython-313.pyc +0 -0
- pluto/providers/__pycache__/openai_provider.cpython-313.pyc +0 -0
- pluto/providers/claude_provider.py +110 -0
- pluto/providers/ollama_provider.py +90 -0
- pluto/providers/openai_provider.py +95 -0
- pluto/reporters/__init__.py +18 -0
- pluto/reporters/__pycache__/__init__.cpython-313.pyc +0 -0
- pluto/reporters/__pycache__/json_reporter.cpython-313.pyc +0 -0
- pluto/reporters/__pycache__/markdown_reporter.cpython-313.pyc +0 -0
- pluto/reporters/__pycache__/pdf_reporter.cpython-313.pyc +0 -0
- pluto/reporters/__pycache__/terminal_reporter.cpython-313.pyc +0 -0
- pluto/reporters/json_reporter.py +21 -0
- pluto/reporters/markdown_reporter.py +34 -0
- pluto/reporters/pdf_reporter.py +140 -0
- pluto/reporters/terminal_reporter.py +47 -0
- pluto/utils/__init__.py +5 -0
- pluto_ai-1.0.0.dist-info/METADATA +241 -0
- pluto_ai-1.0.0.dist-info/RECORD +34 -0
- pluto_ai-1.0.0.dist-info/WHEEL +5 -0
- pluto_ai-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
|
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']
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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']
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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")
|
pluto/utils/__init__.py
ADDED
|
@@ -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
|
+
[](https://badge.fury.io/py/pluto-security-scanner)
|
|
54
|
+
[](https://opensource.org/licenses/MIT)
|
|
55
|
+
[](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 @@
|
|
|
1
|
+
pluto
|