diff --git a/.gitignore b/.gitignore index 3557e5d..43c331e 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,8 @@ profile_default/ ipython_config.py # pyenv +#don't add database credentials +.env .python-version # pipenv @@ -189,3 +191,7 @@ gradle-app.setting /.vs/ node_modules/ +.env +/api/migrations/0001_initial.py +/api/migrations/0002_codesubmission_incident_id_and_more.py +/api/migrations/0003_codesubmission_report_data.py diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..aafb282 --- /dev/null +++ b/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ScannerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..ace3cd2 --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,77 @@ +# Generated by Django 6.0.3 on 2026-03-30 20:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CodeSubmission', + fields=[ + ('submission_id', models.AutoField(primary_key=True, serialize=False)), + ('submission_name', models.CharField(blank=True, max_length=255, null=True)), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('overall_risk_score', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)), + ('simplified_summary', models.TextField(blank=True, null=True)), + ('detailed_summary', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='CWE', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cwe_id', models.CharField(max_length=50, unique=True)), + ('name', models.TextField()), + ('description', models.TextField(blank=True, null=True)), + ('cvss_version', models.CharField(default='3.1', max_length=10)), + ('average_score', models.DecimalField(decimal_places=2, max_digits=5)), + ('severity', models.CharField(blank=True, max_length=20, null=True)), + ('categories', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='User', + fields=[ + ('user_id', models.AutoField(primary_key=True, serialize=False)), + ('email', models.CharField(max_length=255, unique=True)), + ('password_hash', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='File', + fields=[ + ('file_id', models.AutoField(primary_key=True, serialize=False)), + ('file_name', models.CharField(max_length=255)), + ('file_path', models.TextField()), + ('file_type', models.CharField(blank=True, max_length=100, null=True)), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='api.codesubmission')), + ], + ), + migrations.CreateModel( + name='Threat', + fields=[ + ('threat_id', models.AutoField(primary_key=True, serialize=False)), + ('title', models.CharField(max_length=255)), + ('description', models.TextField(blank=True, null=True)), + ('severity_level', models.CharField(choices=[('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High'), ('Critical', 'Critical')], max_length=10)), + ('severity_score', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)), + ('recommendation', models.TextField(blank=True, null=True)), + ('line_number', models.IntegerField(blank=True, null=True)), + ('file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='threats', to='api.file')), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='threats', to='api.codesubmission')), + ], + ), + migrations.AddField( + model_name='codesubmission', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='api.user'), + ), + ] diff --git a/api/migrations/0002_alter_user_table.py b/api/migrations/0002_alter_user_table.py new file mode 100644 index 0000000..9fb34a5 --- /dev/null +++ b/api/migrations/0002_alter_user_table.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.3 on 2026-04-06 17:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[], + state_operations=[ + migrations.AlterModelTable( + name='user', + table='users', + ), + ], + ), + ] diff --git a/api/migrations/0004_merge_20260422_1651.py b/api/migrations/0004_merge_20260422_1651.py new file mode 100644 index 0000000..2bec492 --- /dev/null +++ b/api/migrations/0004_merge_20260422_1651.py @@ -0,0 +1,14 @@ +# Generated by Django 6.0.3 on 2026-04-22 21:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_alter_user_table'), + ('api', '0003_codesubmission_report_data'), + ] + + operations = [ + ] diff --git a/api/migrations/0005_alter_cwe_options.py b/api/migrations/0005_alter_cwe_options.py new file mode 100644 index 0000000..e201f75 --- /dev/null +++ b/api/migrations/0005_alter_cwe_options.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.3 on 2026-04-22 21:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_merge_20260422_1651'), + ] + + operations = [ + migrations.AlterModelOptions( + name='cwe', + options={'managed': False}, + ), + ] diff --git a/api/migrations/0006_alter_cwe_table.py b/api/migrations/0006_alter_cwe_table.py new file mode 100644 index 0000000..cedc8c5 --- /dev/null +++ b/api/migrations/0006_alter_cwe_table.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.3 on 2026-04-22 22:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_alter_cwe_options'), + ] + + operations = [ + migrations.AlterModelTable( + name='cwe', + table='cwe', + ), + ] diff --git a/api/migrations/0007_alter_codesubmission_table.py b/api/migrations/0007_alter_codesubmission_table.py new file mode 100644 index 0000000..a3d3952 --- /dev/null +++ b/api/migrations/0007_alter_codesubmission_table.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.3 on 2026-04-22 22:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_alter_cwe_table'), + ] + + operations = [ + migrations.AlterModelTable( + name='codesubmission', + table='code_submissions', + ), + ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..3447c39 --- /dev/null +++ b/api/models.py @@ -0,0 +1,98 @@ +from django.db import models + +# ------------------- +# Users +# ------------------- +class User(models.Model): + user_id = models.AutoField(primary_key=True) + email = models.CharField(max_length=255, unique=True) + password_hash = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.email + + class Meta: + db_table = "users" + +# ------------------- +# Code Submissions +# ------------------- +class CodeSubmission(models.Model): + submission_id = models.AutoField(primary_key=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions') + submission_name = models.CharField(max_length=255, null=True, blank=True) + uploaded_at = models.DateTimeField(auto_now_add=True) + + overall_risk_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) + simplified_summary = models.TextField(null=True, blank=True) + detailed_summary = models.TextField(null=True, blank=True) + + scan_status = models.CharField(max_length=50, null=True, blank=True) + risk_level = models.CharField(max_length=20, null=True, blank=True) + incident_id = models.CharField(max_length=100, null=True, blank=True) + report_html_path = models.TextField(null=True, blank=True) + report_data = models.JSONField(null=True, blank=True) + + def __str__(self): + return f"{self.submission_name} by {self.user.email}" + + class Meta: + db_table = "code_submissions" + +# ------------------- +# Files +# ------------------- +class File(models.Model): + file_id = models.AutoField(primary_key=True) + submission = models.ForeignKey(CodeSubmission, on_delete=models.CASCADE, related_name='files') + file_name = models.CharField(max_length=255) + file_path = models.TextField() + file_type = models.CharField(max_length=100, null=True, blank=True) + + def __str__(self): + return self.file_name + +# ------------------- +# Threats +# ------------------- +class Threat(models.Model): + SEVERITY_CHOICES = [ + ('Low', 'Low'), + ('Medium', 'Medium'), + ('High', 'High'), + ('Critical', 'Critical'), + ] + + threat_id = models.AutoField(primary_key=True) + submission = models.ForeignKey(CodeSubmission, on_delete=models.CASCADE, related_name='threats') + file = models.ForeignKey(File, on_delete=models.SET_NULL, null=True, blank=True, related_name='threats') + title = models.CharField(max_length=255) + description = models.TextField(null=True, blank=True) + severity_level = models.CharField(max_length=10, choices=SEVERITY_CHOICES) + severity_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) + recommendation = models.TextField(null=True, blank=True) + line_number = models.IntegerField(null=True, blank=True) + + def __str__(self): + return f"{self.title} ({self.severity_level})" + +# ------------------- +# CWE Reference +# ------------------- +class CWE(models.Model): + cwe_id = models.CharField(max_length=50, unique=True) + name = models.TextField() + description = models.TextField(null=True, blank=True) + cvss_version = models.CharField(max_length=10, default='3.1') + average_score = models.DecimalField(max_digits=5, decimal_places=2) + severity = models.CharField(max_length=20, null=True, blank=True) + categories = models.TextField(null=True, blank=True) + + class Meta: + db_table = 'cwe' + managed = False + + def __str__(self): + return f"{self.cwe_id} - {self.name}" + diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..896f234 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,26 @@ +from rest_framework import serializers +from .models import CodeSubmission, File, Threat + +# ------------------- +# Serializer for submitting new code +# ------------------- +class CodeSubmissionSerializer(serializers.ModelSerializer): + code = serializers.CharField(write_only=True) # user input code + submission_name = serializers.CharField(required=False, allow_blank=True) + + class Meta: + model = CodeSubmission + fields = ['submission_name', 'code'] + + def create(self, validated_data): + # We'll store the "code" as a single File for simplicity + code_text = validated_data.pop('code') + submission = CodeSubmission.objects.create(**validated_data) + File.objects.create( + submission=submission, + file_name=validated_data.get('submission_name', 'unnamed.py'), + file_path='', + file_type='code', + ) + # Optionally, you can attach code_text somewhere (DB or AI service) + return submission \ No newline at end of file diff --git a/api/services/ai_service.py b/api/services/ai_service.py new file mode 100644 index 0000000..d8ea995 --- /dev/null +++ b/api/services/ai_service.py @@ -0,0 +1,13 @@ +import os +from openai import OpenAI +from decouple import config + +client = OpenAI(api_key=config("OPENAI_API_KEY")) + +def ask_ai(user_text): + resp = client.chat.completions.create( + model="gpt-4.1-mini", + messages=[{"role": "user", "content": str(user_text)}], + ) + + return resp.choices[0].message.content \ No newline at end of file diff --git a/api/services/dummy_analysis.py b/api/services/dummy_analysis.py new file mode 100644 index 0000000..8142b40 --- /dev/null +++ b/api/services/dummy_analysis.py @@ -0,0 +1,12 @@ +def run_dummy(code, language): + + return { + "summary" : "this dummy code is better than yours", + "findings" : [ + { + "severity" : "Minimal", + "description" : "Bad code", + "fix" : "Figure it Out" + } + ] + } \ No newline at end of file diff --git a/api/services/incident_report_ai.py b/api/services/incident_report_ai.py new file mode 100644 index 0000000..58d8a93 --- /dev/null +++ b/api/services/incident_report_ai.py @@ -0,0 +1,69 @@ +""" +OpenAI call for structured incident report JSON (template is rendered server-side). +""" +from __future__ import annotations + +import json +from typing import Any + +from openai import OpenAI +from decouple import config + +client = OpenAI(api_key=config("OPENAI_API_KEY")) + +# System message: enforce JSON-only output for downstream parsing. +_SYSTEM = ( + "You are a senior cybersecurity analyst. " + "You must respond with a single valid JSON object only (no markdown, no prose outside JSON). " + "Base every field strictly on the provided passage (user code + tool outputs). " + "If information is missing, use concise placeholders like \"N/A\" or \"Not evidenced in scan data\". " + "Do not fabricate CVEs, exploits, or incidents not supported by the passage." +) + + +def build_incident_report_user_prompt(passage: dict[str, Any]) -> str: + passage_json = json.dumps(passage, ensure_ascii=False, indent=2) + schema = """ +Return a JSON object with exactly these keys (all string values unless noted): + +- severity_level: short label (e.g. "Low", "Medium", "High", "Critical", or "Informational") +- incident_type: short type based on findings (e.g. "Secret exposure", "Injection risk", "Misconfiguration") +- systems_affected: what is at risk in plain language (the analyzed code context) +- discovery_method: how issues were found (mention semgrep/gitleaks only if present in passage) +- status: short status string suitable for an executive summary table +- cvss: object with keys base, threat, environmental, supplemental — each a string score "0.0"-"10.0" or "N/A" +- what_happened: 2-5 sentences describing the situation for a non-technical reader +- impact: array of strings; each item one concrete impact statement +- follow_up_consequences: 2-4 sentences on consequences if the organization follows up on recommendations +- no_follow_up_consequences: 2-4 sentences on consequences if recommendations are not followed +- response_actions: array of objects { "action": string, "details": string } with 4-8 practical remediation steps + +Also incorporate this analytical requirement into the narrative fields (what_happened, impact, follow_up_consequences, no_follow_up_consequences): +Analyze this passage and tell me the consequences of following up and not following up. + +Passage (JSON): +""" + return schema.strip() + "\n" + passage_json + + +def generate_incident_report_ai_payload(passage: dict[str, Any]) -> str: + """ + Calls the chat completion API and returns raw message content (expected JSON object). + """ + user_prompt = build_incident_report_user_prompt(passage) + kwargs: dict[str, Any] = { + "model": "gpt-5.4-nano", + "messages": [ + {"role": "system", "content": _SYSTEM}, + {"role": "user", "content": user_prompt}, + ], + "response_format": {"type": "json_object"}, + } + try: + resp = client.chat.completions.create(**kwargs) + except Exception: + # Some deployments/models may reject response_format; retry without it. + kwargs.pop("response_format", None) + resp = client.chat.completions.create(**kwargs) + + return (resp.choices[0].message.content or "").strip() diff --git a/api/services/user_services.py b/api/services/user_services.py new file mode 100644 index 0000000..8ba9d0f --- /dev/null +++ b/api/services/user_services.py @@ -0,0 +1,9 @@ +from django.contrib.auth.models import User + +def create_user(email, password): + user = User.objects.create_user( + username=email, + email=email, + password=password + ) + return user \ No newline at end of file diff --git a/api/tasks.py b/api/tasks.py new file mode 100644 index 0000000..457d809 --- /dev/null +++ b/api/tasks.py @@ -0,0 +1,43 @@ +from .models import CodeSubmission, File, Threat +from .services.dummy_analysis import run_dummy + +def run_analysis_sync(submission_id): + """ + Runs code analysis on a given CodeSubmission. + Updates submission with dummy results and creates Threat records. + """ + + submission = CodeSubmission.objects.get(submission_id=submission_id) + + # For status tracking, we could add a 'status' field if desired + # submission.status = "RUNNING" + # submission.save() + + try: + # Assume analyzing the first file for simplicity + file_obj = submission.files.first() + code_text = "" # replace with actual file reading if needed + + # Call dummy analysis + results = run_dummy(code_text, "python") # language can be dynamic + + # Save results in submission (simplified summary + detailed) + submission.simplified_summary = results.get("summary", "") + submission.detailed_summary = str(results.get("findings", [])) + submission.save() + + # Create Threat objects from findings + for finding in results.get("findings", []): + Threat.objects.create( + submission=submission, + file=file_obj, + title=finding.get("description", "Unnamed Threat"), + description=finding.get("description", ""), + severity_level=finding.get("severity", "Low"), + recommendation=finding.get("fix", ""), + ) + + except Exception as e: + # handle failures, e.g., logging + print(f"Analysis failed for submission {submission_id}: {e}") + # optionally mark submission as failed \ No newline at end of file diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..d248458 --- /dev/null +++ b/api/tests.py @@ -0,0 +1,58 @@ +from django.test import TestCase + +# Create your tests here. +from rest_framework.test import APITestCase +from django.contrib.auth import get_user_model +from rest_framework import status +from .models import CodeSubmission, File, Threat +from .tasks import run_analysis_sync + +User = get_user_model() + +class CodeSubmissionTests(APITestCase): + + def setUp(self): + # create a test user + self.user = User.objects.create( + email="testuser@example.com", + password_hash="hashedpassword" + ) + # log in manually if using session auth, or skip if using token auth + self.client.force_authenticate(user=self.user) + + def test_create_submission(self): + response = self.client.post("/api/scanner/", { + "submission_name": "hello.py", + "code": "print('Hello World')" + }, format="json") + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn("submission_id", response.data) + + def test_workflow(self): + # create submission + submission = CodeSubmission.objects.create( + user=self.user, + submission_name="hello_again.py" + ) + + # create associated file + File.objects.create( + submission=submission, + file_name="hello_again.py", + file_path="", + file_type="code" + ) + + # run dummy analysis + run_analysis_sync(submission.submission_id) + + # refresh from DB + submission.refresh_from_db() + + # confirm dummy analysis populated summaries + self.assertTrue(submission.simplified_summary) + self.assertTrue(submission.detailed_summary) + + # confirm threats created + self.assertGreaterEqual(submission.threats.count(), 1) diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..619090f --- /dev/null +++ b/api/urls.py @@ -0,0 +1,24 @@ +from django.urls import path +from . import views +from .views import login_view, SubmissionView, SubmissionStatusView, vulnerability_list + +urlpatterns = [ + # Login / Logout + path('login/', views.login_view, name='login'), + path('logout/', views.logout_view, name='logout'), + + # Register + path('register/', views.register_view, name='register'), + + # Dashboard (default root) + path('', views.dashboard_view, name='dashboard'), + + path('submit/', views.submit_code, name='submit_code'), + + # Check status / get results of a submission + path('submission//', SubmissionStatusView.as_view(), name='submission_status'), + + path('vulnerabilities/', views.vulnerability_list, name='vulnerability_list'), + + path("report//", views.report_detail_view, name="report_detail"), +] \ No newline at end of file diff --git a/api/utils/incident_report.py b/api/utils/incident_report.py new file mode 100644 index 0000000..de378bb --- /dev/null +++ b/api/utils/incident_report.py @@ -0,0 +1,142 @@ +""" +Helpers to parse LLM JSON output and merge backend-filled fields for incident reports. +""" +from __future__ import annotations + +import json +import re +import uuid +from typing import Any + +from django.utils import timezone + + +def parse_llm_json(raw: str) -> dict[str, Any]: + """ + Parse JSON from model output. Strips optional ```json ... ``` fences. + """ + text = (raw or "").strip() + if not text: + return {} + + fence = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE) + if fence: + text = fence.group(1).strip() + + try: + data = json.loads(text) + return data if isinstance(data, dict) else {} + except json.JSONDecodeError: + return {} + + +def _as_str(value: Any, default: str = "") -> str: + if value is None: + return default + if isinstance(value, str): + return value + return str(value) + + +def _as_str_list(value: Any) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return [value] if value.strip() else [] + if isinstance(value, list): + out: list[str] = [] + for item in value: + s = _as_str(item).strip() + if s: + out.append(s) + return out + return [] + + +def _normalize_cvss(ai: dict[str, Any]) -> dict[str, str]: + raw = ai.get("cvss") + if not isinstance(raw, dict): + raw = {} + return { + "base": _as_str(raw.get("base"), "N/A"), + "threat": _as_str(raw.get("threat"), "N/A"), + "environmental": _as_str(raw.get("environmental"), "N/A"), + "supplemental": _as_str(raw.get("supplemental"), "N/A"), + } + + +def _normalize_response_actions(ai: dict[str, Any]) -> list[dict[str, str]]: + rows = ai.get("response_actions") + if not isinstance(rows, list): + return [] + out: list[dict[str, str]] = [] + for row in rows: + if not isinstance(row, dict): + continue + action = _as_str(row.get("action")).strip() + details = _as_str(row.get("details")).strip() + if action or details: + out.append({"action": action or "—", "details": details or "—"}) + return out + + +def merge_incident_report_context( + *, + request, + ai: dict[str, Any], + parse_error: str | None = None, +) -> dict[str, Any]: + """ + Backend-owned fields + normalized AI fields for template rendering. + """ + now = timezone.now() + incident_id = f"CYB-{now.year}-{uuid.uuid4().hex[:4].upper()}" + dt_display = now.strftime("%d %B %Y, %H:%M %Z").strip() or now.isoformat() + + user = getattr(request, "user", None) + if user is not None and getattr(user, "is_authenticated", False): + reported_by = user.get_username() or _as_str(getattr(user, "email", ""), "Unknown") + else: + reported_by = "Unknown" + + cvss = _normalize_cvss(ai) + impact = _as_str_list(ai.get("impact")) + response_actions = _normalize_response_actions(ai) + + return { + "report_kicker": "Cybersecurity", + "report_title": "Incident Report", + "report_subtitle": "CODE SECURITY ANALYSIS", + "incident_id": incident_id, + "report_datetime": dt_display, + "reported_by": reported_by, + "severity_level": _as_str(ai.get("severity_level"), "Unknown"), + "incident_type": _as_str(ai.get("incident_type"), "Code security review"), + "systems_affected": _as_str(ai.get("systems_affected"), "Submitted code artifact"), + "discovery_method": _as_str( + ai.get("discovery_method"), + "Automated static analysis (semgrep) and secret scanning (gitleaks)", + ), + "status": _as_str(ai.get("status"), "Analysis complete"), + "cvss_base": cvss["base"], + "cvss_threat": cvss["threat"], + "cvss_environmental": cvss["environmental"], + "cvss_supplemental": cvss["supplemental"], + "what_happened": _as_str( + ai.get("what_happened"), + "No narrative could be generated from the model output.", + ), + "impact_items": impact, + "follow_up_consequences": _as_str(ai.get("follow_up_consequences"), ""), + "no_follow_up_consequences": _as_str(ai.get("no_follow_up_consequences"), ""), + "response_actions": response_actions, + "parse_error": parse_error or "", + } + + +DISCLAIMER_TEXT = ( + "This report is partially generated with the assistance of automated code pre-processing " + "and OpenAI-based report generation. It should not be treated as a substitute for manual " + "security review, professional judgment, or formal penetration testing validation. Any " + "findings, risk ratings, and recommendations should be independently verified before use." +) diff --git a/api/utils/prescan.py b/api/utils/prescan.py new file mode 100644 index 0000000..e6d51ca --- /dev/null +++ b/api/utils/prescan.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + + +def run_semgrep(target_path: str) -> dict: + + path = Path(target_path).resolve() + if not path.exists(): + return {"tool": "semgrep", "error": f"path not found: {target_path}", "results": []} + try: + cmd = [ + sys.executable, "-m", "semgrep", "scan", + "--config", "auto", + "--json", + "--quiet", + str(path), + ] + out = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300, + cwd=os.getcwd(), + ) + if out.returncode != 0 and not out.stdout.strip(): + return { + "tool": "semgrep", + "error": out.stderr.strip() or f"exit code {out.returncode}", + "results": [], + } + data = json.loads(out.stdout) if out.stdout.strip() else {} + return {"tool": "semgrep", "error": None, "results": data.get("results", data)} + except FileNotFoundError: + return {"tool": "semgrep", "error": "semgrep not installed (pip install semgrep)", "results": []} + except subprocess.TimeoutExpired: + return {"tool": "semgrep", "error": "timeout", "results": []} + except json.JSONDecodeError as e: + return {"tool": "semgrep", "error": str(e), "results": []} + + +def run_gitleaks(target_path: str) -> dict: + """Run gitleaks detect on target_path and return JSON results. Empty structure if unavailable.""" + path = Path(target_path).resolve() + if not path.exists(): + return {"tool": "gitleaks", "error": f"path not found: {target_path}", "results": []} + source = str(path) if path.is_dir() else str(path.parent) + try: + cmd = [ + "gitleaks", "detect", + "--source", source, + "--no-git", + "--report-format", "json", + "--report-path", "-", # write JSON to stdout + ] + out = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=120, + ) + # when gitleaks finds secrets exit code may be 1, but stdout still contains JSON + raw = out.stdout.strip() + if not raw: + return {"tool": "gitleaks", "error": None, "results": []} + try: + data = json.loads(raw) + results = data if isinstance(data, list) else data.get("findings", data.get("results", [])) + except json.JSONDecodeError: + results = [] + return {"tool": "gitleaks", "error": None, "results": results} + except FileNotFoundError: + return {"tool": "gitleaks", "error": "gitleaks not installed", "results": []} + except subprocess.TimeoutExpired: + return {"tool": "gitleaks", "error": "timeout", "results": []} + + +def main(): + parser = argparse.ArgumentParser(description="Pre-scan: semgrep + gitleaks -> JSON report") + parser.add_argument( + "input_path", + nargs="?", + default=".", + help="File or directory path to scan (default: current directory)", + ) + parser.add_argument( + "-o", "--output", + default="prescan_report.json", + help="Output JSON file path (default: prescan_report.json)", + ) + args = parser.parse_args() + + input_path = os.path.normpath(args.input_path) + if not os.path.exists(input_path): + print(f"Error: path not found {input_path}", file=sys.stderr) + sys.exit(1) + + report = { + "input_path": os.path.abspath(input_path), + "semgrep": run_semgrep(input_path), + "gitleaks": run_gitleaks(input_path), + } + + out_path = args.output + with open(out_path, "w", encoding="utf-8") as f: + json.dump(report, f, ensure_ascii=False, indent=2) + + print(f"Report written to: {out_path}") + + +if __name__ == "__main__": + main() diff --git a/api/utils/prescan_report.json b/api/utils/prescan_report.json new file mode 100644 index 0000000..63f87d9 --- /dev/null +++ b/api/utils/prescan_report.json @@ -0,0 +1,13 @@ +{ + "input_path": "/Users/zhangtingen/Downloads/V/testquery.py", + "semgrep": { + "tool": "semgrep", + "error": "Using `python -m semgrep` to run Semgrep is deprecated as of 1.38.0. Please simply run `semgrep` instead.", + "results": [] + }, + "gitleaks": { + "tool": "gitleaks", + "error": "gitleaks not installed", + "results": [] + } +} \ No newline at end of file diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..41d97a5 --- /dev/null +++ b/api/views.py @@ -0,0 +1,443 @@ +from decimal import Decimal +from django.contrib.auth.hashers import make_password, check_password +from django.shortcuts import render, redirect, get_object_or_404 +from rest_framework.views import APIView +from rest_framework.response import Response +from django.http import JsonResponse +from django.db.models import Q +from pathlib import Path +import json +import re +import tempfile +import shutil + +from .serializers import CodeSubmissionSerializer +from .tasks import run_analysis_sync +from .services.ai_service import ask_ai +from .services.incident_report_ai import generate_incident_report_ai_payload +from .utils.incident_report import ( + DISCLAIMER_TEXT, + merge_incident_report_context, + parse_llm_json, +) +from .utils.prescan import run_semgrep, run_gitleaks + +from .models import CodeSubmission, File, Threat, CWE, User +from django.db.models import Q +from pathlib import Path +import re +import tempfile +import shutil +from .utils.prescan import run_semgrep, run_gitleaks +from django.shortcuts import render, redirect +from django.contrib.auth.hashers import check_password, make_password + +# Max upload size for scan (bytes). +MAX_UPLOAD_BYTES = 2 * 1024 * 1024 + +def require_login(request): + if "user_id" not in request.session: + return redirect("login") + return None + +def _safe_upload_basename(original_name: str) -> str: + """Return a single path segment safe for writing under a temp directory.""" + base = Path(original_name).name + if not base or base in {".", ".."}: + return "upload.txt" + safe = re.sub(r"[^a-zA-Z0-9._-]", "_", base) + if len(safe) > 200: + stem = Path(safe).stem[:150] + suffix = Path(safe).suffix[:20] + safe = stem + suffix + return safe or "upload.txt" + + +def _read_uploaded_text(uploaded) -> str: + """Read uploaded file as text with a hard size cap.""" + chunk = uploaded.read(MAX_UPLOAD_BYTES + 1) + if len(chunk) > MAX_UPLOAD_BYTES: + raise ValueError( + f"File is too large (max {MAX_UPLOAD_BYTES // (1024 * 1024)} MiB)." + ) + return chunk.decode("utf-8", errors="replace") + + +def _run_incident_scan(request, code: str, source: dict): + """ + Shared pipeline: write code to a temp file, pre-scan, call OpenAI JSON report, render HTML. + source: {"origin": "upload"|"paste", "filename": str} + """ + tmp_dir_path = Path(tempfile.mkdtemp(prefix="autopen_")) + + try: + if source.get("origin") == "upload": + fname = _safe_upload_basename(source.get("filename") or "upload.txt") + else: + fname = "pasted_code.py" + + target_file_path = tmp_dir_path / fname + target_file_path.write_text(code, encoding="utf-8") + + # Create submission record early + user_id = request.session.get("user_id") + if not user_id: + return render( + request, + "index.html", + {"result": "You must be logged in to run a scan."}, + ) + + user = User.objects.get(user_id=user_id) + + submission = CodeSubmission.objects.create( + user=user, + submission_name=fname, + scan_status="Running", + ) + + # Pre-process: run semgrep + gitleaks against the temporary file. + semgrep_report = run_semgrep(str(target_file_path)) + gitleaks_report = run_gitleaks(str(target_file_path)) + + # Keep the prompt size bounded. + max_code_chars = 8000 + truncated = code[:max_code_chars] + trunc_note = "" + if len(code) > max_code_chars: + trunc_note = f"\n\n[NOTE] Code was truncated to the first {max_code_chars} characters." + + # Keep tool findings compact to reduce token usage. + semgrep_results = semgrep_report.get("results", []) or [] + gitleaks_results = gitleaks_report.get("results", []) or [] + semgrep_results = semgrep_results[:20] + gitleaks_results = gitleaks_results[:20] + + # Assemble a single "passage" for OpenAI analysis. + passage = { + "source": source, + "user_code": truncated + trunc_note, + "semgrep": { + "error": semgrep_report.get("error"), + "results": semgrep_results, + }, + "gitleaks": { + "error": gitleaks_report.get("error"), + "results": gitleaks_results, + }, + } + + raw_json = generate_incident_report_ai_payload(passage) + ai_data = parse_llm_json(raw_json) + + parse_error = None + if not ai_data: + parse_error = ( + "The model returned empty or non-JSON output; placeholder values are shown where needed." + ) + ai_data = {} + + # Build simple incident ID + incident_id = f"CYB-{submission.uploaded_at.year}-{submission.submission_id}" + + # If you later save a real HTML file, replace this with the actual path + report_path = f"reports/{incident_id}.html" + + # Safely convert score + base_score_raw = ( + ai_data.get("cvss", {}).get("base") + if isinstance(ai_data.get("cvss"), dict) + else None + ) + + base_score = None + try: + if base_score_raw not in (None, "", "N/A"): + base_score = Decimal(str(base_score_raw)) + except Exception: + base_score = None + + # Update dashboard/report fields + submission.submission_name = fname + submission.risk_level = ai_data.get("severity_level", "Informational") + submission.scan_status = "Completed" + submission.overall_risk_score = base_score + submission.simplified_summary = ai_data.get( + "status", + "No findings evidenced" + ) + submission.detailed_summary = ai_data.get( + "what_happened", + "No additional analysis details available." + ) + submission.incident_id = incident_id + submission.report_html_path = report_path + submission.report_data = ai_data + submission.save() + + ctx = merge_incident_report_context( + request=request, + ai=ai_data, + parse_error=parse_error, + ) + ctx["disclaimer"] = DISCLAIMER_TEXT + ctx["submission_id"] = submission.submission_id + ctx["incident_id"] = incident_id + + return render(request, "incident_report.html", ctx) + + except Exception as e: + return render( + request, + "index.html", + {"result": f"Analysis failed: {type(e).__name__}: {e}"}, + ) + finally: + # Best-effort cleanup of temporary files. + shutil.rmtree(tmp_dir_path, ignore_errors=True) + +def ask_ai_view(request): + if request.method == "POST": + data = json.loads(request.body) + user_text = data.get("message") + + response = ask_ai(user_text) + + return JsonResponse({"response": response}) + +def scan_view(request): + target_path = "/path/to/code" # you could get this from request.POST + report = { + "input_path": str(Path(target_path).resolve()), + "semgrep": run_semgrep(target_path), + "gitleaks": run_gitleaks(target_path), + } + return JsonResponse(report) + +# ------------------- +# Create a new code submission and run analysis +# ------------------- + +class SubmissionView(APIView): + + def post(self, request): + + user_id = request.session.get("user_id") + if not user_id: + return Response({"error": "Authentication required"}, status=401) + + try: + user = User.objects.get(user_id=user_id) + except User.DoesNotExist: + return Response({"error": "User not found"}, status=404) + + serializer = CodeSubmissionSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + # create submission + submission = serializer.save(user=request.user) + + # run analysis (sync for now) + run_analysis_sync(submission.submission_id) + + return Response({ + "submission_id": submission.submission_id, + "simplified_summary": submission.simplified_summary + }, status=201) + + +# ------------------- +# Check status and results of a submission +# ------------------- +class SubmissionStatusView(APIView): + + def get(self, request, submission_id): + user_id = request.session.get("user_id") + if not user_id: + return Response({"error": "Authentication required"}, status=401) + + try: + submission = CodeSubmission.objects.get(submission_id=submission_id, user_id=user_id) + except CodeSubmission.DoesNotExist: + return Response({"error": "Submission not found"}, status=404) + + # If threats exist, include them in the response + threats = [ + { + "title": t.title, + "severity": t.severity_level, + "recommendation": t.recommendation + } + for t in submission.threats.all() + ] + + return Response({ + "submission_id": submission.submission_id, + "simplified_summary": submission.simplified_summary, + "threats": threats + }) + + +# ------------------- +# Login / Logout +# ------------------- +def login_view(request): + error = None + if request.method == "POST": + email = request.POST.get("email") + password = request.POST.get("password") + + if not email or not password: + error = "Enter Email and Password" + else: + try: + user = User.objects.get(email=email) + + if check_password(password, user.password_hash): + request.session["user_id"] = user.user_id + request.session["user_email"] = user.email + return redirect("dashboard") + else: + error = "Invalid Email or Password" + except User.DoesNotExist: + error = "Invalid Email or Password" + return render(request, 'login.html', {'error': error}) + +def logout_view(request): + request.session.flush() + return redirect('login') + +# ------------------- +# Dashboard +# ------------------- +def dashboard_view(request): + auth_redirect = require_login(request) + if auth_redirect: + return auth_redirect + + scans = CodeSubmission.objects.filter( + user_id=request.session["user_id"] + ).order_by("-uploaded_at")[:10] + + return render(request, "index.html", {"scans": scans}) + +# ------------------- +# Dummy Code Submission +# ------------------- +def submit_code(request): + if require_login(request): + return require_login(request) + print("HIT SUBMIT VIEW") + result = None + + if request.method != "POST": + return redirect("dashboard") + + uploaded = request.FILES.get("file") + code_paste = request.POST.get("code", "").strip() + + # Prefer a non-empty file upload over pasted text when both are present. + if uploaded is not None and getattr(uploaded, "size", 0) > 0: + try: + code = _read_uploaded_text(uploaded) + except ValueError as e: + return render(request, "index.html", {"result": str(e)}) + if not code.strip(): + return render( + request, + "index.html", + {"result": "Uploaded file is empty."}, + ) + source = { + "origin": "upload", + "filename": uploaded.name or "upload.txt", + } + return _run_incident_scan(request, code, source) + + if code_paste: + return _run_incident_scan( + request, + code_paste, + {"origin": "paste", "filename": "pasted_code.py"}, + ) + + return render( + request, + "index.html", + {"result": "No code submitted. Upload a file or paste code."}, + ) + +def vulnerability_list(request): + query = request.GET.get('q', '').strip() + severity = request.GET.get('severity', '').strip() + + vulnerabilities = CWE.objects.all() + + # ONLY apply search if actually typed + if query: + vulnerabilities = vulnerabilities.filter( + Q(name__icontains=query) | + Q(description__icontains=query) | + Q(cwe_id__icontains=query) + ) + + # Apply severity filter independently + if severity: + vulnerabilities = vulnerabilities.filter(severity=severity) + + context = { + 'vulnerabilities': vulnerabilities, + 'query': query, + 'selected_severity': severity, + } + + return render(request, 'vulnerabilities.html', context) +# ----------------- +# User Registration +# ----------------- +def register_view(request): + error = None + + if request.method == "POST": + email = request.POST.get("email") + password = request.POST.get("password") + + if not email or not password: + error = "Enter Email and Password" + + elif User.objects.filter(email=email).exists(): + error = "Email Already in Use" + + else: + hashed_pass = make_password(password) + + user = User.objects.create(email=email, password_hash=hashed_pass) + request.session["user_id"] = user.user_id + request.session["user_email"] = user.email + return redirect('dashboard') + + return render(request, 'register.html', {'error': error}) + +def report_detail_view(request, submission_id): + user_id = request.session.get("user_id") + if not user_id: + return redirect("/login/") + + submission = get_object_or_404( + CodeSubmission, + submission_id=submission_id, + user_id=user_id + ) + + ai_data = submission.report_data or {} + + ctx = merge_incident_report_context( + request=request, + ai=ai_data, + parse_error=None, + ) + + ctx["disclaimer"] = DISCLAIMER_TEXT + ctx["submission"] = submission + + return render(request, "incident_report.html", ctx) \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..39149a0 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..d1fcc39 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,142 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 5.0.3. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path +from decouple import config +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'dashboard' + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'api', + 'rest_framework' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'front-end')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = '/static/' + +# if you want to keep front-end/styles & front-end/scripts +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'front-end'), # points to the folder that contains scripts/ and styles/ +] + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': config('DB_NAME'), + 'USER': config('DB_USER'), + 'PASSWORD': config('DB_PASS'), + 'HOST': config('DB_HOST'), + 'PORT': config('DB_PORT', default='5432'), + 'OPTIONS': { + 'sslmode': 'require', + } + } +} diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..e5677dc --- /dev/null +++ b/config/urls.py @@ -0,0 +1,24 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('api.urls')), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..c0a9631 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/front-end/incident_report.html b/front-end/incident_report.html new file mode 100644 index 0000000..6eff57a --- /dev/null +++ b/front-end/incident_report.html @@ -0,0 +1,203 @@ + +{% load static %} + + + + + Incident Report — AutoPen + + + + +
+
+ ← Back to dashboard + +
+ + {% if parse_error %} +
{{ parse_error }}
+ {% endif %} + +
+

{{ report_kicker }}

+

{{ report_title }}

+

{{ report_subtitle }}

+
+ +

Incident overview

+
+ + + + + + + + + + + +
Incident ID{{ incident_id }}
Date & time{{ report_datetime }}
Reported by{{ reported_by }}
Severity level{{ severity_level }}
Incident type{{ incident_type }}
Systems affected{{ systems_affected }}
Discovery method{{ discovery_method }}
Status{{ status }}
+
+ +

CVSS score (0.0–10.0)

+
+ + + + + + + + + + + + + + + + + +
BaseThreatEnvironmentalSupplemental
{{ cvss_base }}{{ cvss_threat }}{{ cvss_environmental }}{{ cvss_supplemental }}
+
+ +

Description & impact

+
+

What happened

+

{{ what_happened|linebreaksbr }}

+ +

Impact

+ {% if impact_items %} +
    + {% for item in impact_items %} +
  • {{ item }}
  • + {% endfor %} +
+ {% else %} +

+ {% endif %} + +

Consequences if following up

+

{% if follow_up_consequences %}{{ follow_up_consequences|linebreaksbr }}{% else %}—{% endif %}

+ +

Consequences if not following up

+

{% if no_follow_up_consequences %}{{ no_follow_up_consequences|linebreaksbr }}{% else %}—{% endif %}

+
+ +

Response & resolution

+
+ + + + + + + + + {% for row in response_actions %} + + + + + {% empty %} + + + + {% endfor %} + +
ActionDetails
{{ row.action }}{{ row.details }}
No remediation steps were returned by the model.
+
+ +

{{ disclaimer }}

+ +
+
Sign:
+
Date:
+
+
+ + + + + diff --git a/front-end/index.html b/front-end/index.html index 3a3ade3..b69130a 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -1,25 +1,159 @@ +{% load static %} - - - Hello World! - - - - + AutoPen Dashboard + + + + + + + + - - - - - - + +
+ + + +
+ + + +
+
+

Penetration Testing Dashboard

+

User: {{ user_email|default:"Unknown User" }}

+
System Status: Active
+
+ {% csrf_token %} + +
+
+ +
+
+

Total Scans

+

{{scans|length}}

+
+
+

Critical Vulnerabilities

+

12

+
+
+

Medium Vulnerabilities

+

34

+
+
+

Low Vulnerabilities

+

56

+
+
- \ No newline at end of file +
+
+

Upload Code for Analysis

+ +
+ {% csrf_token %} + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ + {% if result %} +
+ {{ result }} +
+ {% endif %} +
+
+ +
+

Recent Scan Results

+ + + + + + + + + + + {% for scan in scans %} + + + + + + + {% empty %} + + + + {% endfor %} + +
TargetDateRisk LevelStatus
{{ scan.submission_name|default:"Unnamed submission" }}{{ scan.uploaded_at|date:"m/d/Y" }} + {{ scan.risk_level|default:"N/A" }} + {{ scan.scan_status|default:"Unknown" }}
No recent scans yet.
+
+
+ + + diff --git a/front-end/login.html b/front-end/login.html new file mode 100644 index 0000000..886a640 --- /dev/null +++ b/front-end/login.html @@ -0,0 +1,43 @@ + +{% load static %} + + + Login - AutoPen + + + + + + + + + + + \ No newline at end of file diff --git a/front-end/register.html b/front-end/register.html new file mode 100644 index 0000000..21bb538 --- /dev/null +++ b/front-end/register.html @@ -0,0 +1,44 @@ + +{% load static %} + + + Register - AutoPen + + + + + + + + + + + + \ No newline at end of file diff --git a/front-end/scripts/login.js b/front-end/scripts/login.js new file mode 100644 index 0000000..4657bb0 --- /dev/null +++ b/front-end/scripts/login.js @@ -0,0 +1,38 @@ +// Page fade in +window.addEventListener("load", () => { + document.body.classList.add("loaded"); +}); + +// Animate login box +window.addEventListener("DOMContentLoaded", () => { + const box = document.querySelector(".login-box"); + + setTimeout(() => { + box.classList.add("show"); + }, 150); +}); + +// Button loading state +const form = document.querySelector("form"); +const button = document.querySelector(".login-btn"); + +form.addEventListener("submit", () => { + if (button.innerText.toLowerCase().includes("register")) { + button.innerText = "Creating account..."; + } else { + button.innerText = "Logging in..."; + } + + button.classList.add("loading"); + button.disabled = true; +}); + +// Shake on error (if Django renders error) +window.addEventListener("DOMContentLoaded", () => { + const error = document.querySelector(".error"); + const box = document.querySelector(".login-box"); + + if (error && box) { + box.classList.add("shake"); + } +}); \ No newline at end of file diff --git a/front-end/scripts/main.js b/front-end/scripts/main.js index feeebf0..1385644 100644 --- a/front-end/scripts/main.js +++ b/front-end/scripts/main.js @@ -1,8 +1,18 @@ -function operate(operator) { - var num1 = document.querySelector('#num-1').value; - var num2 = document.querySelector('#num-2').value; - resultLambda = operator(num1, num2); - resultLambda(result => { - document.querySelector('#output').innerText = result; - }); -} +// Fade In +window.addEventListener("load", () => { + document.body.classList.add("loaded"); +}); + +window.addEventListener("DOMContentLoaded", () => { + + const hamburger = document.getElementById("hamburger"); + const sidebar = document.querySelector(".sidebar"); + const main = document.querySelector(".main"); + + hamburger.addEventListener("click", () => { + hamburger.classList.toggle("active"); + sidebar.classList.toggle("active"); + main.classList.toggle("shift"); + }); + +}); \ No newline at end of file diff --git a/front-end/scripts/upload.js b/front-end/scripts/upload.js new file mode 100644 index 0000000..4f27024 --- /dev/null +++ b/front-end/scripts/upload.js @@ -0,0 +1,16 @@ +document.addEventListener("DOMContentLoaded", () => { + const tabButtons = document.querySelectorAll(".tab-btn"); + const tabContents = document.querySelectorAll(".tab-content"); + + tabButtons.forEach(button => { + button.addEventListener("click", () => { + // Remove active state + tabButtons.forEach(btn => btn.classList.remove("active")); + tabContents.forEach(tab => tab.classList.remove("active")); + + // Activate selected tab + button.classList.add("active"); + document.getElementById(button.dataset.tab).classList.add("active"); + }); + }); +}); \ No newline at end of file diff --git a/front-end/styles/incident_report.css b/front-end/styles/incident_report.css new file mode 100644 index 0000000..29edcb1 --- /dev/null +++ b/front-end/styles/incident_report.css @@ -0,0 +1,204 @@ +/* Incident report page — grey + seafoam, minimal */ + +:root { + --bg: #f4f7f6; + --surface: #ffffff; + --text: #334155; + --muted: #64748b; + --seafoam: #5eead4; + --seafoam-dark: #0d9488; + --border: #cbd5e1; + --grey-bar: #e2e8f0; +} + +* { + box-sizing: border-box; +} + +body.incident-report-body { + margin: 0; + min-height: 100vh; + font-family: "Segoe UI", system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} + +.ir-wrap { + max-width: 900px; + margin: 0 auto; + padding: 2.5rem 1.5rem 4rem; +} + +.ir-actions { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.ir-back { + display: inline-block; + font-size: 0.9rem; + color: var(--seafoam-dark); + text-decoration: none; +} + +.ir-back:hover { + text-decoration: underline; +} + +.ir-btn-download { + font-size: 0.9rem; + font-family: inherit; + padding: 0.5rem 1.1rem; + border-radius: 999px; + border: 1px solid var(--seafoam-dark); + background: var(--surface); + color: var(--seafoam-dark); + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.ir-btn-download:hover { + background: rgba(94, 234, 212, 0.25); + color: #0f766e; +} + +.ir-parse-error { + background: #fef3c7; + border: 1px solid #fcd34d; + color: #92400e; + padding: 0.75rem 1rem; + border-radius: 8px; + margin-bottom: 1.5rem; + font-size: 0.9rem; +} + +.ir-header { + border-left: 4px solid var(--seafoam); + padding-left: 1.25rem; + margin-bottom: 2rem; +} + +.ir-kicker { + font-size: 0.75rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); + margin: 0 0 0.35rem; +} + +.ir-title { + font-size: 1.85rem; + font-weight: 600; + margin: 0 0 0.25rem; + color: #1e293b; +} + +.ir-subtitle { + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--seafoam-dark); + margin: 0; +} + +.ir-section-title { + font-size: 1rem; + font-weight: 600; + color: #1e293b; + margin: 2rem 0 0.75rem; + padding-bottom: 0.35rem; + border-bottom: 2px solid var(--seafoam); +} + +.ir-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.25rem 1.5rem; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); +} + +.ir-table { + width: 100%; + border-collapse: collapse; + font-size: 0.92rem; +} + +.ir-table th, +.ir-table td { + padding: 0.65rem 0.85rem; + text-align: left; + border-bottom: 1px solid var(--grey-bar); + vertical-align: top; +} + +.ir-table th { + width: 32%; + font-weight: 600; + color: #475569; + background: #f8fafc; +} + +.ir-table tr:last-child th, +.ir-table tr:last-child td { + border-bottom: none; +} + +.ir-cvss-table th { + width: 25%; + text-align: center; + background: #f1f5f9; +} + +.ir-cvss-table td { + text-align: center; + font-variant-numeric: tabular-nums; +} + +.ir-block h4 { + margin: 1rem 0 0.5rem; + font-size: 0.95rem; + color: #0f766e; +} + +.ir-block p { + margin: 0 0 0.75rem; + white-space: pre-wrap; +} + +.ir-block ul { + margin: 0; + padding-left: 1.25rem; +} + +.ir-block li { + margin-bottom: 0.35rem; +} + +.ir-disclaimer { + margin-top: 2.5rem; + padding: 1rem 1.25rem; + background: #f1f5f9; + border-radius: 8px; + font-size: 0.8rem; + color: var(--muted); + line-height: 1.55; +} + +.ir-sign { + margin-top: 2rem; + display: grid; + gap: 1rem; + font-size: 0.95rem; +} + +.ir-sign-row { + border-bottom: 1px solid var(--border); + padding-bottom: 0.35rem; + min-height: 2rem; +} diff --git a/front-end/styles/login.css b/front-end/styles/login.css new file mode 100644 index 0000000..97025e1 --- /dev/null +++ b/front-end/styles/login.css @@ -0,0 +1,148 @@ +/* ================= LOGIN PAGE BASE ================= */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'Inter', sans-serif; + + background: + radial-gradient(circle at top left, rgba(168, 85, 247, 0.15), transparent 40%), + radial-gradient(circle at bottom right, rgba(124, 58, 237, 0.12), transparent 50%), + linear-gradient(135deg, #000000, #0f172a); + + color: #e5e7eb; + + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + + opacity: 0; + transition: opacity 0.6s ease; +} + +body.loaded { + opacity: 1; +} + +/* ================= LOGIN CONTAINER ================= */ +.login-box { + width: 380px; + padding: 35px; + + background: rgba(17, 24, 39, 0.8); + border: 1px solid rgba(168, 85, 247, 0.2); + border-radius: 12px; + + box-shadow: 0 0 20px rgba(168, 85, 247, 0.15); + + transform: translateY(30px); + opacity: 0; + transition: all 0.5s ease; +} + +.login-box.show { + transform: translateY(0); + opacity: 1; +} + +/* ================= TITLE ================= */ +.login-box h2 { + text-align: center; + margin-bottom: 25px; + + font-weight: 700; + + background: linear-gradient(90deg, #c084fc, #a855f7, #7c3aed); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* ================= INPUTS ================= */ +.input-group { + margin-bottom: 15px; +} + +.input-group input { + width: 100%; + padding: 12px; + + border-radius: 8px; + border: 1px solid rgba(168, 85, 247, 0.2); + + background: rgba(17, 24, 39, 0.6); + color: #e5e7eb; + + transition: 0.25s; +} + +.input-group input:focus { + outline: none; + border-color: #a855f7; + box-shadow: 0 0 10px rgba(168, 85, 247, 0.4); + transform: scale(1.02); +} + +/* ================= BUTTON ================= */ +.login-btn { + width: 100%; + padding: 10px 20px; + + background: rgba(168, 85, 247, 0.85); + color: white; + + border: none; + border-radius: 999px; + + cursor: pointer; + transition: 0.25s; +} + +.login-btn:hover { + background: rgba(168, 85, 247, 1); + transform: translateY(-2px); + box-shadow: 0 0 12px rgba(168, 85, 247, 0.6); +} + +.login-btn.loading { + opacity: 0.7; + cursor: not-allowed; +} + +/* ================= ERROR ================= */ +.error { + color: #fb7185; + text-align: center; + margin-bottom: 10px; +} + +/* ================= REGISTER LINK ================= */ +.register { + text-align: center; + margin-top: 15px; +} + +.register a { + color: #c084fc; + text-decoration: none; +} + +.register a:hover { + text-shadow: 0 0 8px rgba(168, 85, 247, 0.6); +} + +/* ================= SHAKE ANIMATION ================= */ +.shake { + animation: shake 0.3s; +} + +@keyframes shake { + 0% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 50% { transform: translateX(5px); } + 75% { transform: translateX(-5px); } + 100% { transform: translateX(0); } +} \ No newline at end of file diff --git a/front-end/styles/style.css b/front-end/styles/style.css index e69de29..08a5ded 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -0,0 +1,384 @@ +/* ================= RESET ================= */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Inter', sans-serif; +} + +body { + background: + radial-gradient(circle at top left, rgba(168, 85, 247, 0.15), transparent 40%), + radial-gradient(circle at bottom right, rgba(124, 58, 237, 0.12), transparent 50%), + linear-gradient(135deg, #000000, #0f172a); + color: #e5e7eb; + display: flex; + min-height: 100vh; + opacity: 0; + transition: opacity 0.6s ease; +} + +body.loaded { + opacity: 1; +} + +/* ================= SIDEBAR ================= */ +.sidebar { + width: 250px; + height: 100vh; + background: rgba(17, 24, 39, 0.8); + padding: 20px; + border-right: 1px solid rgba(168, 85, 247, 0.2); + + position: fixed; + top: 0; + left: -260px; + transition: left 0.3s ease; + z-index: 999; + + display: flex; + flex-direction: column; +} + +.sidebar.active { + left: 0; +} + +.sidebar h2 { + letter-spacing: 1px; + font-weight: 600; +} + +.sidebar ul { + list-style: none; +} + +.sidebar ul li { + letter-spacing: 0.5px; + font-weight: 500; +} + +.sidebar ul li:hover { + color: #c084fc; + text-shadow: 0 0 8px rgba(168, 85, 247, 0.6); +} + +/* ================= SIDEBAR FOOTER ================= */ +.sidebar-footer { + margin-top: auto; + padding: 15px; + border-top: 1px solid rgba(168, 85, 247, 0.2); + + display: flex; + justify-content: space-between; + align-items: center; +} + +.user-name { + font-size: 14px; + color: #c084fc; + font-weight: 500; +} + +.logout-btn { + background-color: #ff4d4d; + color: white; + border: none; + padding: 6px 10px; + border-radius: 5px; + cursor: pointer; + transition: 0.2s; +} + +.logout-btn:hover { + background-color: #ff1f1f; +} + +/* ================= MAIN ================= */ +.main { + flex: 1; + padding: 40px; + margin-left: 0; + transition: margin-left 0.3s ease; +} + +.main.shift { + margin-left: 250px; +} + +/* ================= HEADER ================= */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 40px; +} + +.header h1 { + font-weight: 700; + background: linear-gradient(90deg, #c084fc, #a855f7, #7c3aed); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.status { + box-shadow: 0 0 10px rgba(168, 85, 247, 0.3); + display: flex; + align-items: center; + gap: 10px; +} + +/* Green glowing dot */ +.status-dot { + width: 10px; + height: 10px; + background: #22c55e; + border-radius: 50%; + box-shadow: 0 0 6px #22c55e, 0 0 12px #22c55e; + animation: pulse 1.8s infinite ease-in-out; +} + +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 1; + box-shadow: 0 0 6px #22c55e, 0 0 12px #22c55e; + } + 50% { + transform: scale(1.3); + opacity: 0.7; + box-shadow: 0 0 10px #22c55e, 0 0 20px #22c55e; + } + 100% { + transform: scale(1); + opacity: 1; + box-shadow: 0 0 6px #22c55e, 0 0 12px #22c55e; + } +} + +/* ================= UPLOAD SECTION ================= */ +.code-center { + margin-bottom: 50px; +} + +.code-box { + background: rgba(17, 24, 39, 0.8); + border: 1px solid rgba(168, 85, 247, 0.2); + border-radius: 12px; + padding: 30px; + width: 100%; + max-width: 900px; +} + +.code-box h2 { + color: #ffffff; + margin-bottom: 20px; +} + +/* ================= TABS ================= */ +.upload-tabs { + display: flex; + gap: 12px; + margin-bottom: 20px; +} + +.tab-btn { + background: transparent; + border: 1px solid rgba(168, 85, 247, 0.2); + color: #e9d5ff; + padding: 8px 22px; + border-radius: 999px; + cursor: pointer; + transition: 0.25s; +} + +.tab-btn:hover { + background: rgba(168, 85, 247, 0.15); +} + +.tab-btn.active { + background: rgba(168, 85, 247, 0.25); +} + +/* ================= TAB CONTENT ================= */ +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* ================= DROP ZONE ================= */ +.drop-zone { + display: block; + padding: 50px; + border: 2px dashed rgba(168, 85, 247, 0.8); + border-radius: 12px; + background: rgba(17, 24, 39, 0.6); + text-align: center; + cursor: pointer; + transition: 0.25s; + width: 80%; + max-width: 600px; + margin: 0 auto; +} + +.drop-zone:hover { + background: rgba(17, 24, 39, 0.9); +} + +.drop-zone p { + font-size: 18px; + margin-bottom: 6px; +} + +.drop-zone span { + font-size: 14px; + color: #c4b5fd; +} + +/* ================= TEXTAREA ================= */ +textarea { + width: 100%; + min-height: 220px; + background: rgba(17, 24, 39, 0.6); + color: #ffffff; + border: 1px solid rgba(168, 85, 247, 0.2); + border-radius: 8px; + padding: 15px; + resize: vertical; +} + +/* ================= BUTTON ================= */ +.scan-btn { + margin-top: 20px; + padding: 10px 30px; + background: rgba(168, 85, 247, 0.85); + color: white; + border: none; + border-radius: 999px; + cursor: pointer; + transition: 0.25s; +} + +.scan-btn:hover { + background: rgba(168, 85, 247, 1); +} + +/* ================= CARDS ================= */ +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +.card { + background: rgba(17, 24, 39, 0.8); + border: 1px solid rgba(168, 85, 247, 0.2); + padding: 20px; + border-radius: 12px; + transition: 0.25s; +} + +.card:hover { + background: rgba(17, 24, 39, 0.95); +} + +.card h3 { + color: #ffffff; + margin-bottom: 10px; +} + +.card p { + color: #c084fc; + text-shadow: 0 0 10px rgba(168, 85, 247, 0.4); +} + +/* ================= TABLE ================= */ +.recent-scans { + background: rgba(17, 24, 39, 0.8); + border: 1px solid rgba(168, 85, 247, 0.2); + padding: 20px; + border-radius: 12px; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 15px; +} + +th, td { + padding: 12px; + text-align: left; +} + +th { + color: #e9d5ff; + border-bottom: 1px solid rgba(168, 85, 247, 0.2); +} + +tr:hover { + background: rgba(168, 85, 247, 0.05); +} + +/* ================= HAMBURGER MENU ================= */ +.hamburger { + position: fixed; + top: 20px; + left: 20px; + width: 30px; + height: 22px; + display: flex; + flex-direction: column; + justify-content: space-between; + cursor: pointer; + z-index: 1000; +} + +.hamburger span { + height: 3px; + width: 100%; + background: white; + border-radius: 2px; + transition: 0.3s; +} + +.hamburger.active span:nth-child(1) { + transform: rotate(45deg) translateY(8px); +} + +.hamburger.active span:nth-child(2) { + opacity: 0; +} + +.hamburger.active span:nth-child(3) { + transform: rotate(-45deg) translateY(-8px); +} + +/* ================= SPLIT LAYOUT ================= */ +.split-layout { + display: flex; + gap: 20px; + align-items: flex-start; +} + +/* Upload box */ +.split-layout .code-box { + flex: 2; +} +/* ================= SEVERITY COLORS ================= */ +.Critical { color: #fb7185; font-weight: bold; } +.High { color: #f97316; font-weight: bold; } +.Medium { color: #facc15; font-weight: bold; } +.Low { color: #34d399; font-weight: bold; } + +input, select { + background: rgba(17, 24, 39, 0.6); + color: #ffffff; + border: 1px solid rgba(168, 85, 247, 0.2); + border-radius: 8px; + padding: 10px; +} diff --git a/front-end/vulnerabilities.html b/front-end/vulnerabilities.html new file mode 100644 index 0000000..9cec22b --- /dev/null +++ b/front-end/vulnerabilities.html @@ -0,0 +1,124 @@ + +{% load static %} + + + Vulnerabilities + + + + + + + +
+ + + +
+ + + + +
+ + +
+

Vulnerability Database

+
Live Data
+
+ + +
+

Search & Filter

+ +
+ + + + + + + +
+
+ + +
+

All Vulnerabilities

+ + + + + + + + + + + + + + + {% for v in vulnerabilities %} + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
CWENameCategoryCVSSScoreSeverity
{{ v.cwe_id }} + {{ v.name }} + {{ v.categories }}{{ v.cvss_version }}{{ v.average_score }} + {{ v.severity }} +
No vulnerabilities found.
+
+ +
+ + + \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index cedf85d..0000000 --- a/main.py +++ /dev/null @@ -1,16 +0,0 @@ -import eel - -eel.init('front-end') - - -@eel.expose -def add(num1, num2): - return int(num1) + int(num2) - - -@eel.expose -def subtract(num1, num2): - return int(num1) - int(num2) - - -eel.start('index.html', size=(1000, 600)) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..8e7ac79 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt index e7c13db..86b0c91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -eel -pyqrcode -pyinstaller -pypng -autopep8 +Django>=4.2 +psycopg2-binary>=2.9 +python-dotenv>=1.0 +bcrypt>=4.0s \ No newline at end of file diff --git a/test_cases/edge1.py b/test_cases/edge1.py new file mode 100644 index 0000000..f355037 --- /dev/null +++ b/test_cases/edge1.py @@ -0,0 +1,16 @@ +# Never use eval(user_input) here. +import os + +EXAMPLE_TEXT = "password" +HELP_MESSAGE = "Enter your api key here" + +def docs(): + return EXAMPLE_TEXT + HELP_MESSAGE + + +def add(a, b): + return a + b + + +def fixed_command(): + os.system("ls") \ No newline at end of file diff --git a/test_cases/edhe2.js b/test_cases/edhe2.js new file mode 100644 index 0000000..2c8f8b3 --- /dev/null +++ b/test_cases/edhe2.js @@ -0,0 +1,3 @@ +function renderStatic() { + document.getElementById("box").innerHTML = "Hello"; + } \ No newline at end of file diff --git a/test_cases/mixed1.py b/test_cases/mixed1.py new file mode 100644 index 0000000..6ee0950 --- /dev/null +++ b/test_cases/mixed1.py @@ -0,0 +1,24 @@ +import os +import sqlite3 +import hashlib + +API_KEY = os.getenv("API_KEY") + +def safe_lookup(username): + conn = sqlite3.connect("users.db") + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE username = ?", (username,)) + return cursor.fetchall() + +def unsafe_lookup(username): + conn = sqlite3.connect("users.db") + cursor = conn.cursor() + query = f"SELECT * FROM users WHERE username = '{username}'" + cursor.execute(query) + return cursor.fetchall() + +def weak_hash(password): + return hashlib.sha1(password.encode()).hexdigest() + +def safe_echo(text): + return text \ No newline at end of file diff --git a/test_cases/mixed2.js b/test_cases/mixed2.js new file mode 100644 index 0000000..cfc07c6 --- /dev/null +++ b/test_cases/mixed2.js @@ -0,0 +1,11 @@ +function renderSafe(msg) { + document.getElementById("safe").textContent = msg; +} + +function renderUnsafe(msg) { + document.getElementById("unsafe").innerHTML = msg; +} + +function calculate(a, b) { + return a + b; +} \ No newline at end of file diff --git a/test_cases/safe1.py b/test_cases/safe1.py new file mode 100644 index 0000000..11b1ce7 --- /dev/null +++ b/test_cases/safe1.py @@ -0,0 +1,7 @@ +import sqlite3 + +def get_user(username): + conn = sqlite3.connect("users.db") + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE username = ?", (username,)) + return cursor.fetchall() \ No newline at end of file diff --git a/test_cases/safe2.py b/test_cases/safe2.py new file mode 100644 index 0000000..2288b7e --- /dev/null +++ b/test_cases/safe2.py @@ -0,0 +1,6 @@ +import os + +API_KEY = os.getenv("API_KEY") + +def connect(): + return API_KEY \ No newline at end of file diff --git a/test_cases/safe3.py b/test_cases/safe3.py new file mode 100644 index 0000000..8b12626 --- /dev/null +++ b/test_cases/safe3.py @@ -0,0 +1,6 @@ +import hashlib +import os + +def hash_password(password): + salt = os.urandom(16) + return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100000) \ No newline at end of file diff --git a/test_cases/safe4.c b/test_cases/safe4.c new file mode 100644 index 0000000..fd5e85c --- /dev/null +++ b/test_cases/safe4.c @@ -0,0 +1,10 @@ +#include +#include + +void copy_input(char *input) { + char buffer[10]; + strncpy(buffer, input, sizeof(buffer) - 1); + buffer[sizeof(buffer) - 1] = '\0'; + printf("%s\n", buffer); +} + diff --git a/test_cases/vulnerable1.py b/test_cases/vulnerable1.py new file mode 100644 index 0000000..db52e22 --- /dev/null +++ b/test_cases/vulnerable1.py @@ -0,0 +1,14 @@ +import os +API_KEY = "sk_test_123456789SECRET" +DB_PASSWORD = "supersecretpassword" + + +def connect(): + return f"Connecting with {API_KEY}" + +def ping_host(host): + os.system("ping -c 1 " + host) + + + + diff --git a/test_cases/vulnerable2.py b/test_cases/vulnerable2.py new file mode 100644 index 0000000..9ea15f9 --- /dev/null +++ b/test_cases/vulnerable2.py @@ -0,0 +1,8 @@ +import sqlite3 + +def get_user(username): + conn = sqlite3.connect("users.db") + cursor = conn.cursor() + query = f"SELECT * FROM users WHERE username = '{username}'" + cursor.execute(query) + return cursor.fetchall() \ No newline at end of file diff --git a/test_cases/vulnerable3.c b/test_cases/vulnerable3.c new file mode 100644 index 0000000..a30fbe5 --- /dev/null +++ b/test_cases/vulnerable3.c @@ -0,0 +1,7 @@ +#include +#include +void copy_input(char *input) { + char buffer[10]; + strcpy(buffer, input); + printf("%s\n", buffer); +} diff --git a/test_cases/vulnerable4.js b/test_cases/vulnerable4.js new file mode 100644 index 0000000..25d4bff --- /dev/null +++ b/test_cases/vulnerable4.js @@ -0,0 +1,7 @@ +function showMessage(msg) { + document.getElementById("output").innerHTML = msg; + } + +function showMessage(msg) { + document.getElementById("output").innerHTML = msg; + } \ No newline at end of file