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/README.md b/README.md index b65e37a..8f612ea 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,87 @@ -# Install +# Code security scanning and vulnerability reports -```pip install -r requirements.txt ``` +A Django app for user sign-up, sign-in, and a code upload and analysis flow. The pipeline runs **Semgrep** and **Gitleaks** as a pre-scan, then uses **OpenAI** to generate risk summaries and security-incident style reports, plus vulnerability lists and per-submission report pages. -# Run +## Tech stack -```python main.py``` +| Area | Details | +| --- | --- | +| Backend | Python, Django 4.2+, Django REST framework | +| Database | PostgreSQL (`sslmode=require`) | +| Frontend | Static HTML, CSS, and JavaScript (`front-end/`) | +| Scanning | Semgrep (`--config auto`), Gitleaks (optional; if missing, the step surfaces an error and continues per implementation) | +| AI | OpenAI API (e.g. `gpt-4.1-mini`—see code) | +## Project layout (overview) + +- `config/` – Django project settings and URL config +- `api/` – Models, views, serializers, scanning and AI services (`services/`, `utils/`) +- `front-end/` – Pages and static assets (`styles/`, `scripts/`) +- `test_cases/` – Sample code for testing +- `manage.py` – Django management entry point + +## Environment variables + +Create a `.env` in the project root (or use your `python-decouple` loading pattern). You need at least: + +| Variable | Purpose | +| --- | --- | +| `SECRET_KEY` | Django secret key | +| `DB_NAME` | PostgreSQL database name | +| `DB_USER` | Database user | +| `DB_PASS` | Database password | +| `DB_HOST` | Hostname | +| `DB_PORT` | Port (optional, default `5432`) | +| `OPENAI_API_KEY` | OpenAI API key for analysis and report content | +| `MANAGER_SETUP_CODE` | Registration code used to bootstrap manager accounts | +| `OPENAI_REPORT_MODEL` | Default model for report generation | +| `OPENAI_MODEL_CHOICES` | Comma-separated model list shown in Settings | + +## System dependencies (scanners) + +- **Semgrep** – Install via `pip install semgrep` or the [official method](https://semgrep.dev/docs/getting-started) so `python -m semgrep` works. +- **Gitleaks** – If the `gitleaks` binary is on your `PATH`, pre-scan runs it; if not, that step returns a message and the rest of the flow may still continue. + +## Install and run + +1. **Create a virtual environment** (recommended) and install dependencies: + + ```bash + python -m venv .venv + source .venv/bin/activate # Windows: .venv\Scripts\activate + pip install -r requirements.txt + ``` + +2. **Database** – Ensure PostgreSQL is reachable and `.env` matches your instance. + +3. **Migrate and start the dev server:** + + ```bash + python manage.py migrate + python manage.py runserver + ``` + +4. Open `http://127.0.0.1:8000/` in a browser. You must **register and sign in** before using the dashboard and upload scan. + +## Main routes (reference) + +| Path | Description | +| --- | --- | +| `/` | Dashboard (auth required) | +| `/login/`, `/logout/` | Sign in, sign out | +| `/register/` | Registration | +| `/submit/` | Submit code for scanning | +| `/submission//` | Submission status / results (API view) | +| `/vulnerabilities/` | Vulnerability list | +| `/report//` | Report for one submission | +| `/admin/` | Django admin | + +Single-file uploads are limited to **2 MiB** (see `MAX_UPLOAD_BYTES` in the app); larger files are rejected. + +## Admin + +Create a superuser to use `/admin/`: + +```bash +python manage.py createsuperuser +``` 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/context_processors.py b/api/context_processors.py new file mode 100644 index 0000000..726bd69 --- /dev/null +++ b/api/context_processors.py @@ -0,0 +1,18 @@ +def session_user(request): + """Expose logged-in session email for nav/sidebar templates.""" + theme = "dark" + user_id = request.session.get("user_id") + if user_id: + try: + from .models import UserSetting + + theme = UserSetting.objects.filter(user_id=user_id).values_list("theme", flat=True).first() or theme + except Exception: + theme = "dark" + return { + "user_email": request.session.get("user_email") or "", + "user_name": request.session.get("user_name") or "", + "user_department": request.session.get("department") or "", + "user_role": request.session.get("user_role") or "", + "user_theme": theme, + } diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..a59279a --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 4.2.26 on 2026-04-27 23:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + 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)), + ], + options={ + 'db_table': 'cwe', + 'managed': False, + }, + ), + 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)), + ('scan_status', models.CharField(blank=True, max_length=50, null=True)), + ('risk_level', models.CharField(blank=True, max_length=20, null=True)), + ('incident_id', models.CharField(blank=True, max_length=100, null=True)), + ('report_html_path', models.TextField(blank=True, null=True)), + ('report_data', models.JSONField(blank=True, null=True)), + ], + options={ + 'db_table': 'code_submissions', + }, + ), + 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='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)), + ], + options={ + 'db_table': 'users', + }, + ), + 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_user_account_status_user_department_user_full_name_and_more.py b/api/migrations/0002_user_account_status_user_department_user_full_name_and_more.py new file mode 100644 index 0000000..ac6359e --- /dev/null +++ b/api/migrations/0002_user_account_status_user_department_user_full_name_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.26 on 2026-04-30 20:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='account_status', + field=models.CharField(choices=[('pending', 'Pending approval'), ('active', 'Active'), ('rejected', 'Rejected')], default='pending', max_length=16), + ), + migrations.AddField( + model_name='user', + name='department', + field=models.CharField(choices=[('frontend', 'Frontend Department'), ('backend', 'Backend Department'), ('database', 'Database Department'), ('cybersecurity', 'Cybersecurity Department')], default='frontend', max_length=32), + ), + migrations.AddField( + model_name='user', + name='full_name', + field=models.CharField(default='', max_length=120), + ), + migrations.AddField( + model_name='user', + name='role', + field=models.CharField(choices=[('member', 'Member'), ('manager', 'Manager')], default='member', max_length=16), + ), + migrations.CreateModel( + name='DepartmentJoinRequest', + fields=[ + ('request_id', models.AutoField(primary_key=True, serialize=False)), + ('requested_department', models.CharField(choices=[('frontend', 'Frontend Department'), ('backend', 'Backend Department'), ('database', 'Database Department'), ('cybersecurity', 'Cybersecurity Department')], max_length=32)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=16)), + ('reviewed_at', models.DateTimeField(blank=True, null=True)), + ('review_note', models.CharField(blank=True, default='', max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_join_requests', to='api.user')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='join_requests', to='api.user')), + ], + options={ + 'db_table': 'department_join_requests', + }, + ), + ] diff --git a/api/migrations/0003_codesubmission_focus_end_line_and_more.py b/api/migrations/0003_codesubmission_focus_end_line_and_more.py new file mode 100644 index 0000000..86959a7 --- /dev/null +++ b/api/migrations/0003_codesubmission_focus_end_line_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 4.2.26 on 2026-04-30 21:28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_user_account_status_user_department_user_full_name_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='codesubmission', + name='focus_end_line', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='codesubmission', + name='focus_start_line', + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='codesubmission', + name='priority', + field=models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('urgent', 'Urgent')], default='low', max_length=16), + ), + migrations.AddField( + model_name='codesubmission', + name='priority_updated_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='codesubmission', + name='priority_updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='priority_updates', to='api.user'), + ), + migrations.AddField( + model_name='codesubmission', + name='scan_input_type', + field=models.CharField(choices=[('file', 'File upload'), ('paste', 'Pasted code'), ('text', 'Text question')], default='file', max_length=16), + ), + migrations.CreateModel( + name='UserSetting', + fields=[ + ('setting_id', models.AutoField(primary_key=True, serialize=False)), + ('theme', models.CharField(choices=[('dark', 'Dark'), ('purple', 'Purple'), ('blue', 'Blue')], default='dark', max_length=20)), + ('ai_model', models.CharField(blank=True, default='', max_length=120)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='api.user')), + ], + options={ + 'db_table': 'user_settings', + }, + ), + migrations.CreateModel( + name='ReportComment', + fields=[ + ('comment_id', models.AutoField(primary_key=True, serialize=False)), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='api.codesubmission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='report_comments', to='api.user')), + ], + options={ + 'db_table': 'report_comments', + 'ordering': ['created_at'], + }, + ), + ] diff --git a/api/migrations/0004_codesubmission_report_title.py b/api/migrations/0004_codesubmission_report_title.py new file mode 100644 index 0000000..99d772f --- /dev/null +++ b/api/migrations/0004_codesubmission_report_title.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.26 on 2026-04-30 21:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_codesubmission_focus_end_line_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='codesubmission', + name='report_title', + field=models.CharField(blank=True, default='', help_text='User-facing name for this report (optional).', max_length=200), + ), + ] diff --git a/api/migrations/0005_user_llm_token_totals.py b/api/migrations/0005_user_llm_token_totals.py new file mode 100644 index 0000000..a873870 --- /dev/null +++ b/api/migrations/0005_user_llm_token_totals.py @@ -0,0 +1,23 @@ +# Generated manually for OpenAI usage totals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0004_codesubmission_report_title"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="total_llm_prompt_tokens", + field=models.BigIntegerField(default=0), + ), + migrations.AddField( + model_name="user", + name="total_llm_completion_tokens", + field=models.BigIntegerField(default=0), + ), + ] 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..b354ea4 --- /dev/null +++ b/api/models.py @@ -0,0 +1,229 @@ +from django.db import models + +# ------------------- +# Users +# ------------------- +class User(models.Model): + DEPARTMENT_FRONTEND = "frontend" + DEPARTMENT_BACKEND = "backend" + DEPARTMENT_DATABASE = "database" + DEPARTMENT_CYBERSECURITY = "cybersecurity" + DEPARTMENT_CHOICES = [ + (DEPARTMENT_FRONTEND, "Frontend Department"), + (DEPARTMENT_BACKEND, "Backend Department"), + (DEPARTMENT_DATABASE, "Database Department"), + (DEPARTMENT_CYBERSECURITY, "Cybersecurity Department"), + ] + + ROLE_MEMBER = "member" + ROLE_MANAGER = "manager" + ROLE_CHOICES = [ + (ROLE_MEMBER, "Member"), + (ROLE_MANAGER, "Manager"), + ] + + STATUS_PENDING = "pending" + STATUS_ACTIVE = "active" + STATUS_REJECTED = "rejected" + STATUS_CHOICES = [ + (STATUS_PENDING, "Pending approval"), + (STATUS_ACTIVE, "Active"), + (STATUS_REJECTED, "Rejected"), + ] + + user_id = models.AutoField(primary_key=True) + email = models.CharField(max_length=255, unique=True) + password_hash = models.TextField() + full_name = models.CharField(max_length=120, default="") + department = models.CharField(max_length=32, choices=DEPARTMENT_CHOICES, default=DEPARTMENT_FRONTEND) + role = models.CharField(max_length=16, choices=ROLE_CHOICES, default=ROLE_MEMBER) + account_status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_PENDING) + created_at = models.DateTimeField(auto_now_add=True) + total_llm_prompt_tokens = models.BigIntegerField(default=0) + total_llm_completion_tokens = models.BigIntegerField(default=0) + + def __str__(self): + return self.email + + class Meta: + db_table = "users" + +# ------------------- +# Code Submissions +# ------------------- +class CodeSubmission(models.Model): + PRIORITY_LOW = "low" + PRIORITY_MEDIUM = "medium" + PRIORITY_URGENT = "urgent" + PRIORITY_CHOICES = [ + (PRIORITY_LOW, "Low"), + (PRIORITY_MEDIUM, "Medium"), + (PRIORITY_URGENT, "Urgent"), + ] + + INPUT_FILE = "file" + INPUT_PASTE = "paste" + INPUT_TEXT = "text" + INPUT_CHOICES = [ + (INPUT_FILE, "File upload"), + (INPUT_PASTE, "Pasted code"), + (INPUT_TEXT, "Text question"), + ] + + 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) + report_title = models.CharField( + max_length=200, + blank=True, + default="", + help_text="User-facing name for this report (optional).", + ) + 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) + priority = models.CharField(max_length=16, choices=PRIORITY_CHOICES, default=PRIORITY_LOW) + priority_updated_by = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True, related_name="priority_updates" + ) + priority_updated_at = models.DateTimeField(null=True, blank=True) + scan_input_type = models.CharField(max_length=16, choices=INPUT_CHOICES, default=INPUT_FILE) + focus_start_line = models.PositiveIntegerField(null=True, blank=True) + focus_end_line = models.PositiveIntegerField(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}" + + +class DepartmentJoinRequest(models.Model): + STATUS_PENDING = "pending" + STATUS_APPROVED = "approved" + STATUS_REJECTED = "rejected" + STATUS_CHOICES = [ + (STATUS_PENDING, "Pending"), + (STATUS_APPROVED, "Approved"), + (STATUS_REJECTED, "Rejected"), + ] + + request_id = models.AutoField(primary_key=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="join_requests") + requested_department = models.CharField(max_length=32, choices=User.DEPARTMENT_CHOICES) + status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_PENDING) + reviewed_by = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True, related_name="reviewed_join_requests" + ) + reviewed_at = models.DateTimeField(null=True, blank=True) + review_note = models.CharField(max_length=255, blank=True, default="") + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "department_join_requests" + + def __str__(self): + return f"{self.user.email} -> {self.requested_department} ({self.status})" + + +class ReportComment(models.Model): + comment_id = models.AutoField(primary_key=True) + submission = models.ForeignKey(CodeSubmission, on_delete=models.CASCADE, related_name="comments") + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="report_comments") + comment = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "report_comments" + ordering = ["created_at"] + + def __str__(self): + return f"{self.user.email} on {self.submission_id}" + + +class UserSetting(models.Model): + THEME_DARK = "dark" + THEME_PURPLE = "purple" + THEME_BLUE = "blue" + THEME_CHOICES = [ + (THEME_DARK, "Dark"), + (THEME_PURPLE, "Purple"), + (THEME_BLUE, "Blue"), + ] + + setting_id = models.AutoField(primary_key=True) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="settings") + theme = models.CharField(max_length=20, choices=THEME_CHOICES, default=THEME_DARK) + ai_model = models.CharField(max_length=120, blank=True, default="") + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "user_settings" + + def __str__(self): + return f"Settings for {self.user.email}" + 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..f38c435 --- /dev/null +++ b/api/services/ai_service.py @@ -0,0 +1,15 @@ +import os +from openai import OpenAI +from decouple import config + +client = OpenAI(api_key=config("OPENAI_API_KEY")) + +def ask_ai(user_text, model=None): + resp = client.chat.completions.create( + model=model or "gpt-4.1-mini", + messages=[{"role": "user", "content": str(user_text)}], + ) + + text = resp.choices[0].message.content + usage = getattr(resp, "usage", None) + return text, usage \ 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..90d4632 --- /dev/null +++ b/api/services/incident_report_ai.py @@ -0,0 +1,73 @@ +""" +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], model: str | None = None +) -> tuple[str, Any]: + """ + Calls the chat completion API and returns (message content, usage or None). + """ + user_prompt = build_incident_report_user_prompt(passage) + kwargs: dict[str, Any] = { + "model": model or config("OPENAI_REPORT_MODEL", default="gpt-4.1-mini"), + "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) + + content = (resp.choices[0].message.content or "").strip() + usage = getattr(resp, "usage", None) + return content, usage 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..535b359 --- /dev/null +++ b/api/tests.py @@ -0,0 +1,74 @@ +from datetime import datetime + +from django.test import SimpleTestCase + +# Create your tests here. +from rest_framework.test import APITestCase +from django.contrib.auth import get_user_model +from rest_framework import status +from zoneinfo import ZoneInfo + +from .models import CodeSubmission, File, Threat +from .tasks import run_analysis_sync +from .utils.incident_report import format_report_datetime_chicago + +User = get_user_model() + + +class ReportDatetimeChicagoTests(SimpleTestCase): + def test_formats_utc_in_chicago(self): + dt = datetime(2026, 1, 15, 18, 0, tzinfo=ZoneInfo("UTC")) + s = format_report_datetime_chicago(dt) + self.assertIn("January", s) + self.assertIn("2026", s) + self.assertIn("15", s) + self.assertIn("12:00", s) + + +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..3bfbaba --- /dev/null +++ b/api/urls.py @@ -0,0 +1,32 @@ +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'), + path('scan/', views.start_scan_view, name='start_scan'), + path('reports/', views.reports_view, name='reports'), + path('targets/', views.targets_view, name='targets'), + path('settings/', views.settings_view, name='settings'), + path("personal/", views.personal_info_view, name="personal_info"), + + # Check status / get results of a submission + path('submission//', SubmissionStatusView.as_view(), name='submission_status'), + + path('vulnerabilities/', views.vulnerability_list, name='vulnerability_list'), + path("approvals/", views.approval_queue_view, name="approval_queue"), + + path("report//", views.report_detail_view, name="report_detail"), + + path("assistant/chat/", views.assistant_chat_view, name="assistant_chat"), +] \ 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..df21a54 --- /dev/null +++ b/api/utils/incident_report.py @@ -0,0 +1,165 @@ +""" +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 zoneinfo import ZoneInfo + +from django.utils import timezone + +_CHICAGO_TZ = ZoneInfo("America/Chicago") + + +def format_report_datetime_chicago(dt) -> str: + """ + Format a datetime for incident reports in America/Chicago. + Naive datetimes are treated as UTC (typical when reading legacy rows). + """ + if dt is None: + return "" + if timezone.is_naive(dt): + dt = timezone.make_aware(dt, timezone=ZoneInfo("UTC")) + local = dt.astimezone(_CHICAGO_TZ) + return local.strftime("%d %B %Y, %H:%M %Z").strip() or local.isoformat() + + +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: + # Custom session auth: views may override with submitter from CodeSubmission. + session = getattr(request, "session", None) + if session is not None: + name = (session.get("user_name") or "").strip() + email = (session.get("user_email") or "").strip() + reported_by = name or email or "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/openai_usage.py b/api/utils/openai_usage.py new file mode 100644 index 0000000..bc41da3 --- /dev/null +++ b/api/utils/openai_usage.py @@ -0,0 +1,23 @@ +"""Accumulate OpenAI completion.usage onto the custom User model.""" +from __future__ import annotations + +from typing import Any + +from django.db.models import F + + +def record_openai_usage_for_user(user_id: int | None, usage: Any) -> None: + if not user_id or usage is None: + return + from ..models import User + try: + p = int(getattr(usage, "prompt_tokens", 0) or 0) + c = int(getattr(usage, "completion_tokens", 0) or 0) + except (TypeError, ValueError): + return + if p <= 0 and c <= 0: + return + User.objects.filter(user_id=user_id).update( + total_llm_prompt_tokens=F("total_llm_prompt_tokens") + p, + total_llm_completion_tokens=F("total_llm_completion_tokens") + c, + ) 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..b984e27 --- /dev/null +++ b/api/views.py @@ -0,0 +1,876 @@ +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 django.contrib import messages +from django.utils import timezone +from decouple import config +from rest_framework.views import APIView +from rest_framework.response import Response +from django.http import JsonResponse +from django.views.decorators.http import require_POST, require_http_methods +from django.db.models import Case, IntegerField, Q, When +from pathlib import Path +from typing import Optional +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, + format_report_datetime_chicago, + merge_incident_report_context, + parse_llm_json, +) +from .utils.openai_usage import record_openai_usage_for_user +from .utils.prescan import run_semgrep, run_gitleaks + +from .models import CodeSubmission, File, Threat, CWE, User, DepartmentJoinRequest, ReportComment, UserSetting + +# Max upload size for scan (bytes). +MAX_UPLOAD_BYTES = 2 * 1024 * 1024 +MANAGER_SETUP_CODE = config("MANAGER_SETUP_CODE", default="") +DEFAULT_AI_MODEL = config("OPENAI_REPORT_MODEL", default="gpt-4.1-mini") + +def require_login(request): + if "user_id" not in request.session: + return redirect("login") + return None + + +def _get_session_user(request): + user_id = request.session.get("user_id") + if not user_id: + return None + try: + return User.objects.get(user_id=user_id) + except User.DoesNotExist: + return None + + +def _department_scans_queryset(user): + return CodeSubmission.objects.filter(user__department=user.department) + + +def _my_reports_queryset(user): + return CodeSubmission.objects.filter(user=user) + + +def _active_user_or_redirect(request): + auth_redirect = require_login(request) + if auth_redirect: + return None, auth_redirect + current_user = _get_session_user(request) + if not current_user or current_user.account_status != User.STATUS_ACTIVE: + request.session.flush() + return None, redirect("login") + return current_user, None + + +def _get_or_create_settings(user): + settings, _ = UserSetting.objects.get_or_create(user=user, defaults={"ai_model": DEFAULT_AI_MODEL}) + if not settings.ai_model: + settings.ai_model = DEFAULT_AI_MODEL + settings.save(update_fields=["ai_model", "updated_at"]) + return settings + + +def _available_ai_models(): + raw = config("OPENAI_MODEL_CHOICES", default="") + models = [item.strip() for item in raw.split(",") if item.strip()] + if DEFAULT_AI_MODEL not in models: + models.insert(0, DEFAULT_AI_MODEL) + return models + + +def _parse_focus_lines(request): + start_raw = request.POST.get("focus_start_line", "").strip() + end_raw = request.POST.get("focus_end_line", "").strip() + if not start_raw and not end_raw: + return None, None, None + try: + start = int(start_raw) + end = int(end_raw) + except ValueError: + return None, None, "Line range must use numbers." + if start < 1 or end < start: + return None, None, "Line range must start at 1 and end after the start line." + return start, end, None + + +def _user_can_update_priority(user, submission): + return submission.user_id == user.user_id or ( + user.role == User.ROLE_MANAGER and submission.user.department == user.department + ) + + +def _user_can_rename_report(user, submission): + return submission.user_id == user.user_id + + +def _clean_report_title(request) -> str: + title = (request.POST.get("report_title") or "").strip() + return title[:200] + +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, + *, + scan_input_type: str = CodeSubmission.INPUT_FILE, + focus_start_line: Optional[int] = None, + focus_end_line: Optional[int] = None, + report_title: str = "", +): + """ + 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") + elif source.get("origin") == "text": + fname = _safe_upload_basename(source.get("filename") or "security_question.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) + user_settings = _get_or_create_settings(user) + + submission = CodeSubmission.objects.create( + user=user, + submission_name=fname, + report_title=report_title[:200] if report_title else "", + scan_status="Running", + scan_input_type=scan_input_type, + focus_start_line=focus_start_line, + focus_end_line=focus_end_line, + ) + + # 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." + focus_code = "" + if focus_start_line and focus_end_line: + lines = code.splitlines() + focus_code = "\n".join(lines[focus_start_line - 1:focus_end_line]) + + # 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, + "focus_lines": { + "start": focus_start_line, + "end": focus_end_line, + "code": focus_code, + }, + "semgrep": { + "error": semgrep_report.get("error"), + "results": semgrep_results, + }, + "gitleaks": { + "error": gitleaks_report.get("error"), + "results": gitleaks_results, + }, + } + + raw_json, llm_usage = generate_incident_report_ai_payload(passage, model=user_settings.ai_model) + + record_openai_usage_for_user(user_id, llm_usage) + + 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() + + if parse_error: + messages.warning(request, parse_error) + return redirect("report_detail", submission_id=submission.submission_id) + + 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) + +@require_POST +def assistant_chat_view(request): + """JSON chat for the floating assistant (session auth).""" + if not request.session.get("user_id"): + return JsonResponse({"error": "Authentication required"}, status=401) + try: + data = json.loads(request.body.decode("utf-8") or "{}") + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON"}, status=400) + user_text = (data.get("message") or "").strip() + if not user_text: + return JsonResponse({"error": "Message is required"}, status=400) + try: + user = User.objects.get(user_id=request.session["user_id"]) + user_settings = _get_or_create_settings(user) + model = user_settings.ai_model or None + reply, usage = ask_ai(user_text, model=model) + record_openai_usage_for_user(request.session.get("user_id"), usage) + except Exception as exc: + return JsonResponse({"error": str(exc)}, status=502) + return JsonResponse({"reply": reply or ""}) + +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): + current_user = _get_session_user(request) + if not current_user: + return Response({"error": "Authentication required"}, status=401) + + try: + submission = CodeSubmission.objects.get( + submission_id=submission_id, + user__department=current_user.department, + ) + 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): + if user.account_status == User.STATUS_PENDING: + error = "Account is pending manager approval." + return render(request, 'login.html', {'error': error}) + if user.account_status == User.STATUS_REJECTED: + error = "Account request was rejected. Contact your manager." + return render(request, 'login.html', {'error': error}) + request.session["user_id"] = user.user_id + request.session["user_email"] = user.email + request.session["user_name"] = user.full_name + request.session["department"] = user.department + request.session["user_role"] = user.role + 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): + current_user, auth_redirect = _active_user_or_redirect(request) + if auth_redirect: + return auth_redirect + + department_scans = _department_scans_queryset(current_user) + scans = department_scans.select_related("user").order_by("-uploaded_at")[:5] + urgent_count = department_scans.filter(priority=CodeSubmission.PRIORITY_URGENT).count() + my_count = department_scans.filter(user=current_user).count() + + return render( + request, + "index.html", + { + "scans": scans, + "total_scans": department_scans.count(), + "urgent_count": urgent_count, + "my_count": my_count, + }, + ) + +# ------------------- +# Dummy Code Submission +# ------------------- +def submit_code(request): + current_user, auth_redirect = _active_user_or_redirect(request) + if auth_redirect: + return auth_redirect + + if request.method != "POST": + return redirect("dashboard") + + uploaded = request.FILES.get("file") + code_paste = request.POST.get("code", "").strip() + text_question = request.POST.get("text_question", "").strip() + report_title = _clean_report_title(request) + focus_start, focus_end, line_error = _parse_focus_lines(request) + if line_error: + return render(request, "scan.html", {"result": line_error}) + + # 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, + scan_input_type=CodeSubmission.INPUT_FILE, + focus_start_line=focus_start, + focus_end_line=focus_end, + report_title=report_title, + ) + + if code_paste: + return _run_incident_scan( + request, + code_paste, + {"origin": "paste", "filename": "pasted_code.py"}, + scan_input_type=CodeSubmission.INPUT_PASTE, + focus_start_line=focus_start, + focus_end_line=focus_end, + report_title=report_title, + ) + + if text_question: + return _run_incident_scan( + request, + text_question, + {"origin": "text", "filename": "security_question.txt"}, + scan_input_type=CodeSubmission.INPUT_TEXT, + report_title=report_title, + ) + + return render( + request, + "scan.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) + + +def start_scan_view(request): + current_user, auth_redirect = _active_user_or_redirect(request) + if auth_redirect: + return auth_redirect + if request.method == "POST": + return submit_code(request) + return render(request, "scan.html") + + +def reports_view(request): + current_user, auth_redirect = _active_user_or_redirect(request) + if auth_redirect: + return auth_redirect + + if request.method == "POST": + submission = get_object_or_404( + CodeSubmission, + submission_id=request.POST.get("submission_id"), + user=current_user, + ) + action = request.POST.get("action") + if action == "priority": + priority = request.POST.get("priority") + valid_priorities = {value for value, _ in CodeSubmission.PRIORITY_CHOICES} + if priority in valid_priorities and _user_can_update_priority(current_user, submission): + submission.priority = priority + submission.priority_updated_by = current_user + submission.priority_updated_at = timezone.now() + submission.save(update_fields=["priority", "priority_updated_by", "priority_updated_at"]) + elif action == "comment": + comment = request.POST.get("comment", "").strip() + if comment: + ReportComment.objects.create(submission=submission, user=current_user, comment=comment) + elif action == "rename": + if _user_can_rename_report(current_user, submission): + submission.report_title = _clean_report_title(request) + submission.save(update_fields=["report_title"]) + return redirect("reports") + + reports = ( + _my_reports_queryset(current_user) + .select_related("user", "priority_updated_by") + .prefetch_related("comments__user") + .order_by("-uploaded_at") + ) + return render( + request, + "reports.html", + { + "reports": reports, + "priority_choices": CodeSubmission.PRIORITY_CHOICES, + "current_user": current_user, + }, + ) + + +def targets_view(request): + current_user, auth_redirect = _active_user_or_redirect(request) + if auth_redirect: + return auth_redirect + + if request.method == "POST": + submission = get_object_or_404( + CodeSubmission, + submission_id=request.POST.get("submission_id"), + user__department=current_user.department, + ) + action = request.POST.get("action") + if action == "priority": + priority = request.POST.get("priority") + valid_priorities = {value for value, _ in CodeSubmission.PRIORITY_CHOICES} + if priority in valid_priorities and _user_can_update_priority(current_user, submission): + submission.priority = priority + submission.priority_updated_by = current_user + submission.priority_updated_at = timezone.now() + submission.save(update_fields=["priority", "priority_updated_by", "priority_updated_at"]) + elif action == "comment": + comment = request.POST.get("comment", "").strip() + if comment: + ReportComment.objects.create(submission=submission, user=current_user, comment=comment) + return redirect("targets") + + priority_rank = Case( + When(priority=CodeSubmission.PRIORITY_URGENT, then=0), + When(priority=CodeSubmission.PRIORITY_MEDIUM, then=1), + When(priority=CodeSubmission.PRIORITY_LOW, then=2), + default=99, + output_field=IntegerField(), + ) + reports = ( + _department_scans_queryset(current_user) + .select_related("user", "priority_updated_by") + .prefetch_related("comments__user") + .annotate(priority_rank=priority_rank) + .order_by("priority_rank", "-uploaded_at") + ) + return render( + request, + "targets.html", + { + "reports": reports, + "priority_choices": CodeSubmission.PRIORITY_CHOICES, + "current_user": current_user, + }, + ) + + +def settings_view(request): + current_user, auth_redirect = _active_user_or_redirect(request) + if auth_redirect: + return auth_redirect + + settings = _get_or_create_settings(current_user) + if request.method == "POST": + theme = request.POST.get("theme", "").strip() + ai_model = request.POST.get("ai_model", "").strip() + valid_themes = {value for value, _ in UserSetting.THEME_CHOICES} + if theme in valid_themes: + settings.theme = theme + if ai_model in _available_ai_models(): + settings.ai_model = ai_model + settings.save() + messages.success(request, "Settings saved.") + return redirect("settings") + + return render( + request, + "settings.html", + { + "settings": settings, + "theme_choices": UserSetting.THEME_CHOICES, + "ai_models": _available_ai_models(), + }, + ) + + +def personal_info_view(request): + """Display name, password change, and accumulated OpenAI token usage.""" + current_user, auth_redirect = _active_user_or_redirect(request) + if auth_redirect: + return auth_redirect + + if request.method == "POST": + action = request.POST.get("action", "").strip() + if action == "profile": + full_name = request.POST.get("full_name", "").strip() + if not full_name: + messages.error(request, "Display name cannot be empty.") + elif len(full_name) > 120: + messages.error(request, "Display name is too long (max 120 characters).") + else: + current_user.full_name = full_name + current_user.save(update_fields=["full_name"]) + request.session["user_name"] = full_name + messages.success(request, "Display name updated.") + return redirect("personal_info") + if action == "password": + current_pw = request.POST.get("current_password", "") + new_pw = request.POST.get("new_password", "") + confirm_pw = request.POST.get("confirm_password", "") + if not check_password(current_pw, current_user.password_hash): + messages.error(request, "Current password is incorrect.") + elif len(new_pw) < 8: + messages.error(request, "New password must be at least 8 characters.") + elif new_pw != confirm_pw: + messages.error(request, "New password and confirmation do not match.") + else: + current_user.password_hash = make_password(new_pw) + current_user.save(update_fields=["password_hash"]) + messages.success(request, "Password changed.") + return redirect("personal_info") + return redirect("personal_info") + + db_user = User.objects.get(user_id=current_user.user_id) + p = int(db_user.total_llm_prompt_tokens or 0) + c = int(db_user.total_llm_completion_tokens or 0) + return render( + request, + "personal_info.html", + { + "profile_user": db_user, + "llm_prompt_tokens": p, + "llm_completion_tokens": c, + "llm_total_tokens": p + c, + }, + ) + + +# ----------------- +# User Registration +# ----------------- +def register_view(request): + error = None + department_choices = User.DEPARTMENT_CHOICES + + if request.method == "POST": + full_name = request.POST.get("full_name", "").strip() + email = request.POST.get("email") + password = request.POST.get("password") + department = request.POST.get("department", "").strip() + manager_code = request.POST.get("manager_code", "").strip() + valid_departments = {value for value, _ in User.DEPARTMENT_CHOICES} + + if not full_name or not email or not password or not department: + error = "Please fill in name, email, password, and department." + elif department not in valid_departments: + error = "Invalid department selected." + + elif User.objects.filter(email=email).exists(): + error = "Email Already in Use" + + else: + hashed_pass = make_password(password) + is_manager = bool(MANAGER_SETUP_CODE) and manager_code == MANAGER_SETUP_CODE + user = User.objects.create( + full_name=full_name, + email=email, + password_hash=hashed_pass, + department=department, + role=User.ROLE_MANAGER if is_manager else User.ROLE_MEMBER, + account_status=User.STATUS_ACTIVE if is_manager else User.STATUS_PENDING, + ) + if not is_manager: + DepartmentJoinRequest.objects.create( + user=user, + requested_department=department, + status=DepartmentJoinRequest.STATUS_PENDING, + ) + return render( + request, + 'register.html', + { + 'error': None, + 'success': "Account created. Waiting for department manager approval.", + 'department_choices': department_choices, + }, + ) + request.session["user_id"] = user.user_id + request.session["user_email"] = user.email + request.session["user_name"] = user.full_name + request.session["department"] = user.department + request.session["user_role"] = user.role + return redirect('dashboard') + + return render(request, 'register.html', {'error': error, 'department_choices': department_choices}) + +@require_http_methods(["GET", "HEAD"]) +def report_detail_view(request, submission_id): + current_user = _get_session_user(request) + if not current_user: + return redirect("/login/") + + submission = get_object_or_404( + CodeSubmission.objects.select_related("user"), + submission_id=submission_id, + user__department=current_user.department + ) + + ai_data = submission.report_data or {} + + ctx = merge_incident_report_context( + request=request, + ai=ai_data, + parse_error=None, + ) + + submitter = submission.user + ctx["reported_by"] = (submitter.full_name or "").strip() or submitter.email or "Unknown" + ctx["report_datetime"] = format_report_datetime_chicago(submission.uploaded_at) + if submission.incident_id: + ctx["incident_id"] = submission.incident_id + + ctx["disclaimer"] = DISCLAIMER_TEXT + ctx["submission"] = submission + + return render(request, "incident_report.html", ctx) + + +def approval_queue_view(request): + current_user = _get_session_user(request) + if not current_user: + return redirect("login") + if current_user.role != User.ROLE_MANAGER: + return redirect("dashboard") + + if request.method == "POST": + request_id = request.POST.get("request_id") + action = request.POST.get("action") + join_req = get_object_or_404( + DepartmentJoinRequest, + request_id=request_id, + requested_department=current_user.department, + status=DepartmentJoinRequest.STATUS_PENDING, + ) + if action == "approve": + join_req.status = DepartmentJoinRequest.STATUS_APPROVED + join_req.reviewed_by = current_user + join_req.reviewed_at = timezone.now() + join_req.save() + join_req.user.account_status = User.STATUS_ACTIVE + join_req.user.department = join_req.requested_department + join_req.user.save() + elif action == "reject": + join_req.status = DepartmentJoinRequest.STATUS_REJECTED + join_req.reviewed_by = current_user + join_req.reviewed_at = timezone.now() + join_req.save() + join_req.user.account_status = User.STATUS_REJECTED + join_req.user.save() + return redirect("approval_queue") + + pending_requests = DepartmentJoinRequest.objects.filter( + requested_department=current_user.department, + status=DepartmentJoinRequest.STATUS_PENDING, + ).select_related("user").order_by("-created_at") + return render( + request, + "approval_queue.html", + { + "pending_requests": pending_requests, + "department_label": current_user.get_department_display(), + }, + ) \ 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..74ea462 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,143 @@ +""" +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', + 'api.context_processors.session_user', + ], + }, + }, +] + +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/approval_queue.html b/front-end/approval_queue.html new file mode 100644 index 0000000..bf01452 --- /dev/null +++ b/front-end/approval_queue.html @@ -0,0 +1,57 @@ + +{% load static %} + + + + + Department Approvals — AutoPen + + + + +{% include "includes/app_sidebar.html" %} + +
+
+

Department Approvals

+
{{ department_label }}
+
+ +
+

Pending Requests

+ + + + + + + + + + + {% for req in pending_requests %} + + + + + + + {% empty %} + + + + {% endfor %} + +
NameEmailRequested AtAction
{{ req.user.full_name }}{{ req.user.email }}{{ req.created_at|date:"m/d/Y H:i" }} +
+ {% csrf_token %} + + + +
+
No pending department requests.
+
+
+{% include "includes/ai_assistant.html" %} + + diff --git a/front-end/images/ai-assistant.png b/front-end/images/ai-assistant.png new file mode 100644 index 0000000..60c2e8a Binary files /dev/null and b/front-end/images/ai-assistant.png differ diff --git a/front-end/incident_report.html b/front-end/incident_report.html new file mode 100644 index 0000000..d4afbc1 --- /dev/null +++ b/front-end/incident_report.html @@ -0,0 +1,225 @@ + +{% load static %} + + + + + Incident Report — AutoPen + + + + + + + +{% include "includes/app_sidebar.html" %} + +
+
+
+ ← Back to dashboard + +
+ + {% if parse_error %} +
{{ parse_error }}
+ {% endif %} + + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + +
+

{{ report_kicker }}

+

{{ report_title }}

+

{{ report_subtitle }}

+ {% if submission.report_title %} +

Your report name: {{ submission.report_title }}

+ {% endif %} +
+ +

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:
+
+
+
+ + + +{% include "includes/ai_assistant.html" %} + + diff --git a/front-end/includes/ai_assistant.html b/front-end/includes/ai_assistant.html new file mode 100644 index 0000000..0411bd5 --- /dev/null +++ b/front-end/includes/ai_assistant.html @@ -0,0 +1,180 @@ +{% load static %} +{# FAB dock + panel are siblings; both appended to body so nothing clips fixed panel. Visibility via inline display !important (survives CSS cache issues). #} +
+ +
+ + diff --git a/front-end/includes/app_sidebar.html b/front-end/includes/app_sidebar.html new file mode 100644 index 0000000..b10db2c --- /dev/null +++ b/front-end/includes/app_sidebar.html @@ -0,0 +1,50 @@ +{% load static %} +{% with u=request.resolver_match.url_name|default_if_none:"" %} + + + + +{% endwith %} diff --git a/front-end/index.html b/front-end/index.html index 3a3ade3..42e3570 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -1,25 +1,127 @@ - - - - - - - - - Hello World! - - - - - - - - - - - - - - - - \ No newline at end of file + +{% load static %} + + + + + + + AutoPen Dashboard + + + + + + + + + + + +{% include "includes/app_sidebar.html" %} + +
+
+

Penetration Testing Dashboard

+

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

+
System Status: Active
+
+ +
+
+

Department Reports

+

{{ total_scans }}

+
+
+

My Reports

+

{{ my_count }}

+
+
+

Urgent Targets

+

{{ urgent_count }}

+
+
+

Department

+

{{ user_department|default:"N/A" }}

+
+
+ +
+
+

Quick Scan

+ +
+ {% csrf_token %} + +
+ +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + + Open advanced scan options +
+ + {% if result %} +
+ {{ result }} +
+ {% endif %} +
+
+ +
+

Recent Department Reports

+ + + + + + + + + + + + + {% for scan in scans %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
TargetDateOwnerRisk LevelPriorityStatus
{% if scan.report_title %}{{ scan.report_title }}{% else %}{{ scan.submission_name|default:"Unnamed submission" }}{% endif %}{{ scan.uploaded_at|date:"m/d/Y" }}{{ scan.user.full_name|default:scan.user.email }} + {{ scan.risk_level|default:"N/A" }} + {{ scan.get_priority_display }}{{ scan.scan_status|default:"Unknown" }}
No recent scans yet.
+
+
+ +{% include "includes/ai_assistant.html" %} + + 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/personal_info.html b/front-end/personal_info.html new file mode 100644 index 0000000..6fc6bae --- /dev/null +++ b/front-end/personal_info.html @@ -0,0 +1,77 @@ + +{% load static %} + + + + + Personal information — AutoPen + + + + +{% include "includes/app_sidebar.html" %} + +
+
+

Personal information

+
Account & usage
+
+ +
+ {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + +
+

Account

+

Email (sign-in): {{ profile_user.email }}

+
+ {% csrf_token %} + + + +
+
+ +
+

Change password

+
+ {% csrf_token %} + + + + + +
+
+ +
+

OpenAI token usage (this account)

+

Totals accumulate from report scans and the in-app assistant when the API returns usage data. Older activity may show as zero.

+ + + + + + +
Prompt tokens (cumulative){{ llm_prompt_tokens }}
Completion tokens (cumulative){{ llm_completion_tokens }}
Total (prompt + completion){{ llm_total_tokens }}
+
+
+
+{% include "includes/ai_assistant.html" %} + + diff --git a/front-end/register.html b/front-end/register.html new file mode 100644 index 0000000..8c1847f --- /dev/null +++ b/front-end/register.html @@ -0,0 +1,65 @@ + +{% load static %} + + + Register - AutoPen + + + + + + + + + + + + \ No newline at end of file diff --git a/front-end/reports.html b/front-end/reports.html new file mode 100644 index 0000000..308bfa3 --- /dev/null +++ b/front-end/reports.html @@ -0,0 +1,100 @@ + +{% load static %} + + + + + My Reports — AutoPen + + + + +{% include "includes/app_sidebar.html" %} + +
+
+

My Reports

+
Your scans only · newest first
+
+ +
+ + + + + + + + + + + + + + {% for report in reports %} + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Report nameTimeRiskUrgencyStatusType
+ + {% if report.report_title %}{{ report.report_title }}{% else %}{{ report.submission_name|default:"Untitled" }}{% endif %} + +
+ {% csrf_token %} + + + + +
+
{{ report.uploaded_at|date:"m/d/Y H:i" }} + {{ report.risk_level|default:"—" }} + +
+ {% csrf_token %} + + + + +
+
{{ report.scan_status|default:"—" }}{{ report.get_scan_input_type_display }} + Open +
+
+ {% for comment in report.comments.all %} +

{{ comment.user.full_name|default:comment.user.email }}: {{ comment.comment }}

+ {% empty %} +

No comments yet.

+ {% endfor %} +
+
+ {% csrf_token %} + + + + +
+
No reports yet. Start a scan from Dashboard or Start Scan.
+
+
+{% include "includes/ai_assistant.html" %} + + diff --git a/front-end/scan.html b/front-end/scan.html new file mode 100644 index 0000000..65ea849 --- /dev/null +++ b/front-end/scan.html @@ -0,0 +1,75 @@ + +{% load static %} + + + + + Start Scan — AutoPen + + + + + +{% include "includes/app_sidebar.html" %} + +
+
+

Start Scan

+
Advanced Analysis
+
+ +
+

Submit Code or Ask AI

+
+ {% csrf_token %} + +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ + +
+ + {% if result %} +
{{ result }}
+ {% endif %} +
+
+{% include "includes/ai_assistant.html" %} + + 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..ef04ed0 100644 --- a/front-end/scripts/main.js +++ b/front-end/scripts/main.js @@ -1,8 +1,52 @@ -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"); + + if (!hamburger || !sidebar) { + return; + } + + const syncAria = () => { + const open = sidebar.classList.contains("active"); + hamburger.setAttribute("aria-expanded", open ? "true" : "false"); + }; + + syncAria(); + + hamburger.addEventListener("click", () => { + hamburger.classList.toggle("active"); + sidebar.classList.toggle("active"); + if (main) { + main.classList.toggle("shift"); + } + syncAria(); + }); + + hamburger.addEventListener("keydown", (ev) => { + if (ev.key === "Enter" || ev.key === " ") { + ev.preventDefault(); + hamburger.click(); + } + }); + + const closeSidebar = () => { + hamburger.classList.remove("active"); + sidebar.classList.remove("active"); + if (main) { + main.classList.remove("shift"); + } + syncAria(); + }; + + sidebar.addEventListener("click", (ev) => { + if (ev.target.closest("a.sidebar-link")) { + closeSidebar(); + } + }); +}); 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/settings.html b/front-end/settings.html new file mode 100644 index 0000000..97077d2 --- /dev/null +++ b/front-end/settings.html @@ -0,0 +1,56 @@ + +{% load static %} + + + + + Settings — AutoPen + + + + +{% include "includes/app_sidebar.html" %} + +
+
+

Settings

+
Personal Preferences
+
+ +
+ {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + +
+ {% csrf_token %} +
+ + + +
+ +

Model choices come from your environment: `OPENAI_MODEL_CHOICES`, with `OPENAI_REPORT_MODEL` as the fallback.

+ +
+
+
+{% include "includes/ai_assistant.html" %} + + diff --git a/front-end/styles/incident_report.css b/front-end/styles/incident_report.css new file mode 100644 index 0000000..a2aa1c2 --- /dev/null +++ b/front-end/styles/incident_report.css @@ -0,0 +1,226 @@ +/* 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; +} + +body.incident-report-body.theme-purple { + background: #f7f0ff; +} + +body.incident-report-body.theme-blue { + background: #eff6ff; +} + +.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-user-report-name { + margin-top: 0.5rem; + font-size: 0.95rem; + color: var(--muted); +} + +.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; +} + +/* Sidebar layout: incident report stays light; `.main--report` stretches full width inside flex body */ +body.incident-report-body .main.main--report { + width: 100%; + flex: 1; + padding-top: 3rem; + background: transparent; +} diff --git a/front-end/styles/login.css b/front-end/styles/login.css new file mode 100644 index 0000000..cff6018 --- /dev/null +++ b/front-end/styles/login.css @@ -0,0 +1,163 @@ +/* ================= 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 select { + 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; +} + +.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; +} + +.success { + color: #86efac; + 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..e49342a 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -0,0 +1,999 @@ +/* ================= 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-nav ul { + list-style: none; +} + +.sidebar-nav ul li { + letter-spacing: 0.5px; + font-weight: 500; + margin-bottom: 4px; +} + +.sidebar-link { + display: block; + padding: 10px 12px; + border-radius: 8px; + color: #e5e7eb; + text-decoration: none; + transition: color 0.2s ease, background 0.2s ease; +} + +.sidebar-link:hover { + color: #c084fc; + background: rgba(168, 85, 247, 0.12); + text-shadow: 0 0 8px rgba(168, 85, 247, 0.35); +} + +.sidebar-link.active { + color: #fdf4ff; + background: rgba(168, 85, 247, 0.22); + border-left: 3px solid #c084fc; + padding-left: 9px; +} + +.sidebar-disabled { + display: block; + padding: 10px 12px; + border-radius: 8px; + color: #6b7280; + cursor: not-allowed; + font-size: 0.95rem; +} + +.sidebar-brand { + letter-spacing: 1px; + font-weight: 600; +} + +.logout-btn-inline { + margin-left: auto; +} + +/* ================= 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; +} + +a.sidebar-profile-link { + text-decoration: none; + max-width: 55%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +a.sidebar-profile-link.active { + color: #f5d0fe; +} + +a.sidebar-profile-link:hover { + text-decoration: underline; + color: #e9d5ff; +} + +/* Personal information page */ +.personal-info-page .personal-section { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(168, 85, 247, 0.2); +} + +.personal-info-page .personal-section:last-child { + border-bottom: none; +} + +.personal-section-title { + font-size: 1.1rem; + margin: 0 0 0.75rem; + color: #e9d5ff; +} + +.personal-form label { + display: block; + margin-bottom: 0.75rem; +} + +.personal-form input[type="text"], +.personal-form input[type="password"] { + display: block; + width: 100%; + max-width: 420px; + margin-top: 0.35rem; + padding: 0.5rem 0.65rem; + border-radius: 6px; + border: 1px solid rgba(148, 163, 184, 0.4); + background: rgba(15, 23, 42, 0.6); + color: #f1f5f9; +} + +.personal-token-table { + width: 100%; + max-width: 480px; + border-collapse: collapse; + font-size: 0.95rem; +} + +.personal-token-table th, +.personal-token-table td { + text-align: left; + padding: 0.5rem 0.75rem; + border: 1px solid rgba(148, 163, 184, 0.25); +} + +.personal-token-table th { + width: 55%; + font-weight: 600; + background: rgba(30, 41, 59, 0.5); +} + +.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; +} + +/* ================= NEW WORKFLOW PAGES ================= */ +body.theme-purple { + background: + radial-gradient(circle at top left, rgba(236, 72, 153, 0.18), transparent 40%), + radial-gradient(circle at bottom right, rgba(168, 85, 247, 0.16), transparent 50%), + linear-gradient(135deg, #090012, #1e1b4b); +} + +body.theme-blue { + background: + radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 40%), + radial-gradient(circle at bottom right, rgba(14, 165, 233, 0.14), transparent 50%), + linear-gradient(135deg, #020617, #0f172a); +} + +.wide-box { + max-width: 1100px; +} + +.secondary-link { + display: inline-block; + margin-left: 14px; + color: #c084fc; + text-decoration: none; +} + +.line-range, +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 18px; +} + +.line-range label, +.settings-grid label { + display: grid; + gap: 8px; + color: #e9d5ff; +} + +.form-message { + margin-top: 18px; + color: #86efac; +} + +.muted { + color: #94a3b8; +} + +.report-list { + display: grid; + gap: 18px; +} + +.report-card { + background: rgba(17, 24, 39, 0.8); + border: 1px solid rgba(168, 85, 247, 0.2); + border-radius: 12px; + padding: 20px; +} + +.report-card-header { + display: flex; + justify-content: space-between; + gap: 20px; + align-items: flex-start; + margin-bottom: 14px; +} + +.report-card h3 a { + color: #ffffff; + text-decoration: none; +} + +.compact-btn { + margin-top: 0; + padding: 8px 16px; +} + +.comment-line { + margin-top: 8px; + color: #d1d5db; +} + +.priority-pill { + border-radius: 999px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.08); + white-space: nowrap; +} + +.priority-urgent { + color: #fb7185; + font-weight: 700; +} + +.priority-medium { + color: #facc15; + font-weight: 700; +} + +.priority-low { + color: #34d399; + font-weight: 700; +} + +.priority-border-urgent { + border-color: rgba(251, 113, 133, 0.65); +} + +.priority-border-medium { + border-color: rgba(250, 204, 21, 0.55); +} + +.priority-border-low { + border-color: rgba(52, 211, 153, 0.45); +} + +/* ================= DEPARTMENT REPORTS TABLE ================= */ +.reports-table-wrap { + overflow-x: auto; +} + +.reports-table { + width: 100%; + border-collapse: collapse; +} + +.reports-table th, +.reports-table td { + vertical-align: top; +} + +.reports-row-main td { + border-bottom: 1px solid rgba(148, 163, 184, 0.25); + padding-top: 14px; + padding-bottom: 14px; +} + +.reports-row-comments td { + border-bottom: 1px solid rgba(168, 85, 247, 0.25); + padding-top: 10px; + padding-bottom: 18px; + background: rgba(15, 23, 42, 0.45); +} + +.reports-title-link { + color: #f8fafc; + font-weight: 600; + text-decoration: none; +} + +.reports-title-link:hover { + text-decoration: underline; +} + +.reports-name-cell { + min-width: 200px; +} + +.reports-rename-form { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; + align-items: center; +} + +.reports-rename-form input[type="text"] { + flex: 1 1 180px; + min-width: 140px; +} + +.reports-inline-priority { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.reports-comments-thread { + margin-bottom: 10px; + max-height: 140px; + overflow-y: auto; +} + +.reports-comment-bar { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.reports-comment-input { + flex: 1 1 240px; + min-width: 200px; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid rgba(168, 85, 247, 0.35); + background: rgba(17, 24, 39, 0.85); + color: #f8fafc; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Targets: department queue + discussion board */ +.targets-table .targets-board-row td { + border-bottom: 1px solid rgba(168, 85, 247, 0.28); + padding: 14px 16px 18px; + background: rgba(15, 23, 42, 0.55); +} + +.targets-board { + max-width: 920px; +} + +.targets-board-header { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #c4b5fd; + margin-bottom: 12px; +} + +.targets-board-thread { + display: flex; + flex-direction: column; + gap: 12px; + max-height: 320px; + overflow-y: auto; + padding-right: 6px; + margin-bottom: 14px; +} + +.targets-board-post { + border-radius: 12px; + padding: 12px 14px; + background: rgba(30, 41, 59, 0.9); + border: 1px solid rgba(148, 163, 184, 0.2); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2); +} + +.targets-board-post-meta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 10px; + margin-bottom: 8px; + font-size: 0.8rem; + color: #94a3b8; +} + +.targets-board-author { + font-weight: 600; + color: #e2e8f0; +} + +.targets-board-post-meta time { + font-variant-numeric: tabular-nums; +} + +.targets-board-body { + color: #f1f5f9; + font-size: 0.95rem; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; +} + +.targets-board-empty { + margin: 0; + padding: 8px 0; +} + +.targets-board-compose { + display: flex; + flex-direction: column; + gap: 10px; +} + +.targets-board-textarea { + width: 100%; + min-height: 72px; + resize: vertical; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid rgba(168, 85, 247, 0.35); + background: rgba(17, 24, 39, 0.92); + color: #f8fafc; + font-family: inherit; + font-size: 0.95rem; + line-height: 1.45; +} + +.targets-board-textarea:focus { + outline: none; + border-color: rgba(192, 132, 252, 0.65); + box-shadow: 0 0 0 2px rgba(168, 85, 247, 0.2); +} + +.targets-board-actions { + display: flex; + justify-content: flex-end; +} + +.quick-report-name { + margin-bottom: 12px; +} + +.quick-report-name input { + width: 100%; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid rgba(168, 85, 247, 0.2); + background: rgba(17, 24, 39, 0.6); + color: #f8fafc; +} + +/* ================= FLOATING AI ASSISTANT ================= */ +/* Dock + panel: fixed to viewport; out of document flow (no layout shift). */ +/* Final position also enforced via inline !important in includes/ai_assistant.html */ +.ai-assistant-dock { + position: fixed !important; + top: auto !important; + left: auto !important; + right: calc(16px + env(safe-area-inset-right, 0px)) !important; + bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important; + width: 56px; + height: 56px; + margin: 0 !important; + padding: 0 !important; + border: none; + background: transparent; + box-shadow: none; + z-index: 2147483000; + font-size: 14px; + display: block; + pointer-events: none; +} + +.ai-assistant-dock .ai-assistant-fab { + pointer-events: auto; +} + +.ai-assistant-fab { + width: 56px; + height: 56px; + border-radius: 50%; + border: 2px solid rgba(192, 132, 252, 0.85); + padding: 0; + cursor: pointer; + background: rgba(17, 24, 39, 0.95); + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(168, 85, 247, 0.25); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.ai-assistant-fab:hover { + transform: scale(1.06); + box-shadow: 0 10px 32px rgba(168, 85, 247, 0.35), 0 0 0 1px rgba(192, 132, 252, 0.5); +} + +.ai-assistant-fab img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +/* Panel is a body sibling (see ai_assistant.html). Open state also sets display via inline !important in script. */ +.ai-assistant-panel { + position: fixed !important; + top: auto !important; + left: auto !important; + right: calc(16px + env(safe-area-inset-right, 0px)) !important; + bottom: calc(80px + env(safe-area-inset-bottom, 0px)) !important; + width: min(360px, calc(100vw - 36px)); + max-height: min(480px, 70vh); + display: none !important; + flex-direction: column; + background: rgba(15, 23, 42, 0.97); + border: 1px solid rgba(168, 85, 247, 0.35); + border-radius: 14px; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55); + overflow: hidden; + z-index: 2147483001; + pointer-events: auto; +} + +.ai-assistant-panel.ai-assistant-panel--open { + display: flex !important; +} + +.ai-assistant-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: rgba(30, 27, 75, 0.6); + color: #f3e8ff; + font-weight: 600; + border-bottom: 1px solid rgba(168, 85, 247, 0.25); + position: relative; + z-index: 2; +} + +.ai-assistant-close { + background: transparent; + border: none; + color: #e9d5ff; + font-size: 22px; + line-height: 1; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + position: relative; + z-index: 3; +} + +.ai-assistant-close:hover { + background: rgba(168, 85, 247, 0.2); +} + +.ai-assistant-messages { + flex: 1; + min-height: 200px; + max-height: 320px; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.ai-assistant-msg { + display: flex; + width: 100%; +} + +.ai-assistant-msg--user { + justify-content: flex-end; +} + +.ai-assistant-msg--assistant { + justify-content: flex-start; +} + +.ai-assistant-bubble { + max-width: 88%; + padding: 8px 12px; + border-radius: 12px; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; +} + +.ai-assistant-msg--user .ai-assistant-bubble { + background: rgba(168, 85, 247, 0.35); + color: #fdf4ff; + border: 1px solid rgba(192, 132, 252, 0.45); +} + +.ai-assistant-msg--assistant .ai-assistant-bubble { + background: rgba(30, 41, 59, 0.95); + color: #e2e8f0; + border: 1px solid rgba(148, 163, 184, 0.35); +} + +.ai-assistant-form { + display: flex; + gap: 8px; + padding: 10px; + border-top: 1px solid rgba(168, 85, 247, 0.2); + background: rgba(2, 6, 23, 0.6); +} + +.ai-assistant-form input[type="text"] { + flex: 1; + min-width: 0; +} + +.ai-assistant-send { + border: none; + border-radius: 999px; + padding: 8px 14px; + background: rgba(168, 85, 247, 0.9); + color: #fff; + cursor: pointer; + font-weight: 600; + white-space: nowrap; +} + +.ai-assistant-send:hover { + background: rgba(168, 85, 247, 1); +} diff --git a/front-end/targets.html b/front-end/targets.html new file mode 100644 index 0000000..b8c7880 --- /dev/null +++ b/front-end/targets.html @@ -0,0 +1,111 @@ + +{% load static %} + + + + + Targets — AutoPen + + + + +{% include "includes/app_sidebar.html" %} + +
+
+

Targets

+
Department queue · urgency first, then newest
+
+ +
+ + + + + + + + + + + + + + + {% for report in reports %} + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Report nameTimeOwnerRiskUrgencyStatusType
+ + {% if report.report_title %}{{ report.report_title }}{% else %}{{ report.submission_name|default:"Untitled" }}{% endif %} + + {{ report.uploaded_at|date:"m/d/Y H:i" }}{{ report.user.full_name|default:report.user.email }} + {{ report.risk_level|default:"—" }} + + {% if report.user_id == current_user.user_id or current_user.role == "manager" %} +
+ {% csrf_token %} + + + + +
+ {% else %} + {{ report.get_priority_display }} + {% endif %} +
{{ report.scan_status|default:"—" }}{{ report.get_scan_input_type_display }} + Open +
+
+
Discussion
+
+ {% for comment in report.comments.all %} +
+ +
{{ comment.comment }}
+
+ {% empty %} +

No messages yet — be the first to leave a note.

+ {% endfor %} +
+
+ {% csrf_token %} + + + + +
+ +
+
+
+
No department reports yet.
+
+
+{% include "includes/ai_assistant.html" %} + + diff --git a/front-end/vulnerabilities.html b/front-end/vulnerabilities.html new file mode 100644 index 0000000..116f78f --- /dev/null +++ b/front-end/vulnerabilities.html @@ -0,0 +1,99 @@ + +{% load static %} + + + + + Vulnerabilities — AutoPen + + + + + + + +{% include "includes/app_sidebar.html" %} + + +
+ + +
+

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.
+
+ +
+ +{% include "includes/ai_assistant.html" %} + + 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..b712172 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -eel -pyqrcode -pyinstaller -pypng -autopep8 +Django>=4.2 +psycopg2-binary>=2.9 +python-dotenv>=1.0 +bcrypt>=4.0.0 +djangorestframework>=3.14 +python-decouple>=3.8 +openai>=1.0.0 \ 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