From 0ed1b94c48ac61aaed7ce616cd73cf3ab0e30242 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 11 Feb 2026 17:41:22 -0600 Subject: [PATCH 01/41] test! --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index cedf85d..994422a 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import eel +#sofia eel.init('front-end') From 69a3118b255aff665e2979e9e76620eb68dc6fe4 Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 11 Feb 2026 17:45:11 -0600 Subject: [PATCH 02/41] Jacob Test --- main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/main.py b/main.py index 994422a..b600c9f 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import eel #sofia +#jacob eel.init('front-end') From 38602780efbbca505da7c3b581f0728f506d8530 Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Wed, 11 Feb 2026 17:56:19 -0600 Subject: [PATCH 03/41] tim --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index b600c9f..aae1daa 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ import eel #sofia #jacob - +#tim eel.init('front-end') From 99a712e2ce34ff082fef4071a1dc454823baec33 Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Thu, 12 Feb 2026 12:59:29 -0600 Subject: [PATCH 04/41] connect openAI api to backend --- .gitignore | 1 + main.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/.gitignore b/.gitignore index 3557e5d..06125d6 100644 --- a/.gitignore +++ b/.gitignore @@ -189,3 +189,4 @@ gradle-app.setting /.vs/ node_modules/ +.env diff --git a/main.py b/main.py index aae1daa..a64024d 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,25 @@ import eel +import os +from dotenv import load_dotenv +from openai import OpenAI #sofia #jacob #tim eel.init('front-end') +load_dotenv() +client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + +def test_openai(): + if not os.getenv("OPENAI_API_KEY"): + print("ERROR: OPENAI_API_KEY not found. Put it in .env (same folder as main.py).") + return + + resp = client.chat.completions.create( + model="gpt-4.1-mini", + messages=[{"role": "user", "content": "Say hello in one sentence."}], + ) + print("OpenAI test reply:", resp.choices[0].message.content) @eel.expose def add(num1, num2): From 3b4c170e00c2cddc1672cdaed1c605a44dc4f203 Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Thu, 12 Feb 2026 21:30:00 -0600 Subject: [PATCH 05/41] connet frontend-backend-openai api --- front-end/index.html | 23 +++++++++++++---------- front-end/scripts/main.js | 11 +++++++++++ main.py | 26 +++++++++++++++++--------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/front-end/index.html b/front-end/index.html index 3a3ade3..e2ab344 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -1,25 +1,28 @@ - - Hello World! - + + - - - - - - + + + +
- \ No newline at end of file + + + + +

+
+
diff --git a/front-end/scripts/main.js b/front-end/scripts/main.js
index feeebf0..56a9f67 100644
--- a/front-end/scripts/main.js
+++ b/front-end/scripts/main.js
@@ -6,3 +6,14 @@ function operate(operator) {
 		document.querySelector('#output').innerText = result;
 	});
 }
+
+function askGPT() {
+	const prompt = document.querySelector('#prompt').value;
+
+	document.querySelector('#output').innerText = "Loading...";
+
+	eel.ask_api(prompt)(result => {
+		document.querySelector('#output').innerText = result;
+	});
+
+}
\ No newline at end of file
diff --git a/main.py b/main.py
index a64024d..7a8aab7 100644
--- a/main.py
+++ b/main.py
@@ -8,18 +8,25 @@
 eel.init('front-end')
 
 load_dotenv()
+print("API key loaded:", bool(os.getenv("OPENAI_API_KEY")))
 client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
 
-def test_openai():
-    if not os.getenv("OPENAI_API_KEY"):
-        print("ERROR: OPENAI_API_KEY not found. Put it in .env (same folder as main.py).")
-        return
+@eel.expose
+def ask_api(user_text):
+    print("ask_api received:", user_text)
 
     resp = client.chat.completions.create(
-        model="gpt-4.1-mini",
-        messages=[{"role": "user", "content": "Say hello in one sentence."}],
-    )
-    print("OpenAI test reply:", resp.choices[0].message.content)
+            model="gpt-4.1-mini",
+            messages=[{"role": "user", "content": str(user_text)}],
+        )
+
+    answer = resp.choices[0].message.content
+    print("ask_api answer:", answer)
+    return answer
+
+
+
+
 
 @eel.expose
 def add(num1, num2):
@@ -31,4 +38,5 @@ def subtract(num1, num2):
     return int(num1) - int(num2)
 
 
-eel.start('index.html', size=(1000, 600))
+if __name__ == "__main__":
+    eel.start('index.html', size=(1000, 600), mode='safari')

From cfb44ee3df396af788b267a816deb3759c25e251 Mon Sep 17 00:00:00 2001
From: NathanEdwards2023 
Date: Sun, 15 Feb 2026 17:07:27 -0600
Subject: [PATCH 06/41] Basic Frontend

---
 front-end/index.html        | 131 +++++++++++++++----
 front-end/scripts/upload.js |  14 +++
 front-end/styles/style.css  | 245 ++++++++++++++++++++++++++++++++++++
 3 files changed, 366 insertions(+), 24 deletions(-)
 create mode 100644 front-end/scripts/upload.js

diff --git a/front-end/index.html b/front-end/index.html
index 3a3ade3..2be9760 100644
--- a/front-end/index.html
+++ b/front-end/index.html
@@ -1,25 +1,108 @@
-
-
-
-
-    
-    
-    
-    
-    Hello World!
-    
-    
-    
-
-
-
-
-
-    
-    
-    
-    
-    
-
-
+
+
+
+    
+    AutoPen Dashboard
+    
+
+    
+    
+
+
+
+    
+
+    
+
+

Penetration Testing Dashboard

+
System Status: Active
+
+ +
+
+

Total Scans

+

128

+
+
+

Critical Vulnerabilities

+

12

+
+
+

Medium Vulnerabilities

+

34

+
+
+

Low Vulnerabilities

+

56

+
+
+ + +
+
+

Upload Code for Analysis

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

Recent Scan Results

+ + + + + + + + + + + + + + + + + + + + + + + +
TargetDateRisk LevelStatus
example.com02/14/2026CriticalCompleted
test-server.net02/12/2026LowCompleted
+
+
+ + \ No newline at end of file diff --git a/front-end/scripts/upload.js b/front-end/scripts/upload.js new file mode 100644 index 0000000..4d33a0b --- /dev/null +++ b/front-end/scripts/upload.js @@ -0,0 +1,14 @@ +const tabButtons = document.querySelectorAll(".tab-btn"); +const tabContents = document.querySelectorAll(".tab-content"); + +tabButtons.forEach(button => { + button.addEventListener("click", () => { + // Remove active state + tabButtons.forEach(btn => btn.classList.remove("active")); + tabContents.forEach(tab => tab.classList.remove("active")); + + // Activate selected tab + button.classList.add("active"); + document.getElementById(button.dataset.tab).classList.add("active"); + }); +}); \ No newline at end of file diff --git a/front-end/styles/style.css b/front-end/styles/style.css index e69de29..2965520 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -0,0 +1,245 @@ +/* ================= RESET ================= */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background: #000000; + color: #e5e7eb; + display: flex; + min-height: 100vh; +} + +/* ================= 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); +} + +.sidebar h2 { + color: #e9d5ff; + margin-bottom: 40px; + text-align: center; +} + +.sidebar ul { + list-style: none; +} + +.sidebar ul li { + padding: 15px; + margin: 10px 0; + background: rgba(17, 24, 39, 0.8); + border: 1px solid rgba(168, 85, 247, 0.2); + border-radius: 8px; + cursor: pointer; + transition: 0.25s; +} + +.sidebar ul li:hover { + background: rgba(17, 24, 39, 0.95); +} + +/* ================= MAIN ================= */ +.main { + flex: 1; + padding: 40px; +} + +/* ================= HEADER ================= */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 40px; +} + +.header h1 { + color: #ffffff; +} + +.status { + background: rgba(17, 24, 39, 0.8); + padding: 10px 20px; + border-radius: 999px; + border: 1px solid rgba(168, 85, 247, 0.2); +} + +/* ================= 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 { + font-size: 28px; + font-weight: bold; +} + +/* ================= 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); +} + +/* ================= SEVERITY COLORS ================= */ +.critical { color: #fb7185; font-weight: bold; } +.medium { color: #facc15; font-weight: bold; } +.low { color: #34d399; font-weight: bold; } \ No newline at end of file From 0ae235de84cd195a6a1de2cdcd6cedc70c61990c Mon Sep 17 00:00:00 2001 From: kaye-s Date: Mon, 16 Feb 2026 16:13:48 -0600 Subject: [PATCH 07/41] database connected to enviornment --- db.py | 21 ++++++++++++++ main.py | 5 ++++ models.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ testquery.py | 27 ++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 db.py create mode 100644 models.py create mode 100644 testquery.py diff --git a/db.py b/db.py new file mode 100644 index 0000000..4212ab5 --- /dev/null +++ b/db.py @@ -0,0 +1,21 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from dotenv import load_dotenv + +load_dotenv() + +DB_URL = f"postgresql://" \ + f"{os.getenv('DB_USER')}:" \ + f"{os.getenv('DB_PASS')}@" \ + f"{os.getenv('DB_HOST')}:" \ + f"{os.getenv('DB_PORT')}/" \ + f"{os.getenv('DB_NAME')}" + +engine = create_engine( + DB_URL, + echo=True, + connect_args={"sslmode": "require"} +) + +SessionLocal = sessionmaker(bind=engine) diff --git a/main.py b/main.py index aae1daa..579b0b6 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,14 @@ import eel +from db import SessionLocal #sofia #jacob #tim eel.init('front-end') +session = SessionLocal() + +print("Connected successfully!") + @eel.expose def add(num1, num2): diff --git a/models.py b/models.py new file mode 100644 index 0000000..6da1d64 --- /dev/null +++ b/models.py @@ -0,0 +1,74 @@ +import os +from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, Numeric, Enum +from sqlalchemy.orm import declarative_base, relationship, sessionmaker +from datetime import datetime +import enum +from dotenv import load_dotenv + +load_dotenv() + +Base = declarative_base() + +# Severity Enum +class SeverityEnum(enum.Enum): + Low = "Low" + Medium = "Medium" + High = "High" + Critical = "Critical" + + +class User(Base): + __tablename__ = "users" + + user_id = Column(Integer, primary_key=True, autoincrement=True) + email = Column(String(255), unique=True, nullable=False) + password_hash = Column(Text, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + + submissions = relationship("CodeSubmission", back_populates="user") + + +class CodeSubmission(Base): + __tablename__ = "code_submissions" + + submission_id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False) + submission_name = Column(String(255)) + uploaded_at = Column(DateTime, default=datetime.utcnow) + overall_risk_score = Column(Numeric(5,2)) + simplified_summary = Column(Text) + detailed_summary = Column(Text) + + user = relationship("User", back_populates="submissions") + files = relationship("File", back_populates="submission") + threats = relationship("Threat", back_populates="submission") + + +class File(Base): + __tablename__ = "files" + + file_id = Column(Integer, primary_key=True, autoincrement=True) + submission_id = Column(Integer, ForeignKey("code_submissions.submission_id", ondelete="CASCADE"), nullable=False) + file_name = Column(String(255), nullable=False) + file_path = Column(Text, nullable=False) + file_type = Column(String(100)) + + submission = relationship("CodeSubmission", back_populates="files") + threats = relationship("Threat", back_populates="file") + + +class Threat(Base): + __tablename__ = "threats" + + threat_id = Column(Integer, primary_key=True, autoincrement=True) + submission_id = Column(Integer, ForeignKey("code_submissions.submission_id", ondelete="CASCADE"), nullable=False) + file_id = Column(Integer, ForeignKey("files.file_id", ondelete="SET NULL"), nullable=True) + title = Column(String(255), nullable=False) + description = Column(Text) + severity_level = Column(Enum(SeverityEnum), nullable=False) + severity_score = Column(Numeric(5,2)) + recommendation = Column(Text) + line_number = Column(Integer) + + submission = relationship("CodeSubmission", back_populates="threats") + file = relationship("File", back_populates="threats") diff --git a/requirements.txt b/requirements.txt index e7c13db..c0be71b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,6 @@ pyqrcode pyinstaller pypng autopep8 +psycopg2-binary +SQLAlchemy +python-dotenv diff --git a/testquery.py b/testquery.py new file mode 100644 index 0000000..cf61dd8 --- /dev/null +++ b/testquery.py @@ -0,0 +1,27 @@ +from sqlalchemy import create_engine, text + +# Replace these with your Supabase info +USER = "postgres" +PASSWORD = "CapstoneVSecurity123" +HOST = "db.zzraywtbowpotrqbevkz.supabase.co" +PORT = "5432" +DATABASE = "postgres" + +# This is the connection URL SQLAlchemy needs +DB_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}" + +# Create a connection +engine = create_engine(DB_URL) + +try: + with engine.connect() as conn: + # Run a simple query to test + result = conn.execute(text("SELECT NOW();")) + print("Connected! Server time:", result.fetchone()[0]) +except Exception as e: + print("Connection failed:", e) + +with engine.connect() as conn: + #conn.execute(text("INSERT INTO users(email, password_hash) VALUES ('test2@example.com', '1A2B3C');")) + result = conn.execute(text("SELECT * FROM users;")) + print(result.fetchall()) \ No newline at end of file From 5f3d3fad0f303676c92e79b6e25dc38983b82e83 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 18 Feb 2026 12:42:46 -0600 Subject: [PATCH 08/41] database secrets preserved --- testquery.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/testquery.py b/testquery.py index cf61dd8..80e1505 100644 --- a/testquery.py +++ b/testquery.py @@ -1,17 +1,5 @@ -from sqlalchemy import create_engine, text - -# Replace these with your Supabase info -USER = "postgres" -PASSWORD = "CapstoneVSecurity123" -HOST = "db.zzraywtbowpotrqbevkz.supabase.co" -PORT = "5432" -DATABASE = "postgres" - -# This is the connection URL SQLAlchemy needs -DB_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}" - -# Create a connection -engine = create_engine(DB_URL) +from sqlalchemy import text +from db import engine try: with engine.connect() as conn: From d85a13a877983ca4d5be09842c740bc15c1b3af1 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 18 Feb 2026 12:43:30 -0600 Subject: [PATCH 09/41] Revert "database connected to enviornment" This reverts commit 0ae235de84cd195a6a1de2cdcd6cedc70c61990c. # Conflicts: # testquery.py --- db.py | 21 -------------- main.py | 5 ---- models.py | 74 ------------------------------------------------ requirements.txt | 3 -- 4 files changed, 103 deletions(-) delete mode 100644 db.py delete mode 100644 models.py diff --git a/db.py b/db.py deleted file mode 100644 index 4212ab5..0000000 --- a/db.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from dotenv import load_dotenv - -load_dotenv() - -DB_URL = f"postgresql://" \ - f"{os.getenv('DB_USER')}:" \ - f"{os.getenv('DB_PASS')}@" \ - f"{os.getenv('DB_HOST')}:" \ - f"{os.getenv('DB_PORT')}/" \ - f"{os.getenv('DB_NAME')}" - -engine = create_engine( - DB_URL, - echo=True, - connect_args={"sslmode": "require"} -) - -SessionLocal = sessionmaker(bind=engine) diff --git a/main.py b/main.py index 579b0b6..aae1daa 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,9 @@ import eel -from db import SessionLocal #sofia #jacob #tim eel.init('front-end') -session = SessionLocal() - -print("Connected successfully!") - @eel.expose def add(num1, num2): diff --git a/models.py b/models.py deleted file mode 100644 index 6da1d64..0000000 --- a/models.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, Numeric, Enum -from sqlalchemy.orm import declarative_base, relationship, sessionmaker -from datetime import datetime -import enum -from dotenv import load_dotenv - -load_dotenv() - -Base = declarative_base() - -# Severity Enum -class SeverityEnum(enum.Enum): - Low = "Low" - Medium = "Medium" - High = "High" - Critical = "Critical" - - -class User(Base): - __tablename__ = "users" - - user_id = Column(Integer, primary_key=True, autoincrement=True) - email = Column(String(255), unique=True, nullable=False) - password_hash = Column(Text, nullable=False) - created_at = Column(DateTime, default=datetime.utcnow) - - submissions = relationship("CodeSubmission", back_populates="user") - - -class CodeSubmission(Base): - __tablename__ = "code_submissions" - - submission_id = Column(Integer, primary_key=True, autoincrement=True) - user_id = Column(Integer, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False) - submission_name = Column(String(255)) - uploaded_at = Column(DateTime, default=datetime.utcnow) - overall_risk_score = Column(Numeric(5,2)) - simplified_summary = Column(Text) - detailed_summary = Column(Text) - - user = relationship("User", back_populates="submissions") - files = relationship("File", back_populates="submission") - threats = relationship("Threat", back_populates="submission") - - -class File(Base): - __tablename__ = "files" - - file_id = Column(Integer, primary_key=True, autoincrement=True) - submission_id = Column(Integer, ForeignKey("code_submissions.submission_id", ondelete="CASCADE"), nullable=False) - file_name = Column(String(255), nullable=False) - file_path = Column(Text, nullable=False) - file_type = Column(String(100)) - - submission = relationship("CodeSubmission", back_populates="files") - threats = relationship("Threat", back_populates="file") - - -class Threat(Base): - __tablename__ = "threats" - - threat_id = Column(Integer, primary_key=True, autoincrement=True) - submission_id = Column(Integer, ForeignKey("code_submissions.submission_id", ondelete="CASCADE"), nullable=False) - file_id = Column(Integer, ForeignKey("files.file_id", ondelete="SET NULL"), nullable=True) - title = Column(String(255), nullable=False) - description = Column(Text) - severity_level = Column(Enum(SeverityEnum), nullable=False) - severity_score = Column(Numeric(5,2)) - recommendation = Column(Text) - line_number = Column(Integer) - - submission = relationship("CodeSubmission", back_populates="threats") - file = relationship("File", back_populates="threats") diff --git a/requirements.txt b/requirements.txt index c0be71b..e7c13db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,3 @@ pyqrcode pyinstaller pypng autopep8 -psycopg2-binary -SQLAlchemy -python-dotenv From 8377edc021735d1ec31fb97bfd7acc565c560162 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 18 Feb 2026 12:49:04 -0600 Subject: [PATCH 10/41] OKAY NOW FIXED, be sure to get .env file updated with new credentials --- db.py | 21 +++++++++++++++++++++ requirements.txt | 3 +++ 2 files changed, 24 insertions(+) create mode 100644 db.py diff --git a/db.py b/db.py new file mode 100644 index 0000000..5afc09b --- /dev/null +++ b/db.py @@ -0,0 +1,21 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from dotenv import load_dotenv + +load_dotenv() + +DB_URL = f"postgresql://" \ + f"{os.getenv('DB_USER')}:" \ + f"{os.getenv('DB_PASS')}@" \ + f"{os.getenv('DB_HOST')}:" \ + f"{os.getenv('DB_PORT')}/" \ + f"{os.getenv('DB_NAME')}" + +engine = create_engine( + DB_URL, + echo=True, + connect_args={"sslmode": "require"} +) + +SessionLocal = sessionmaker(bind=engine) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e7c13db..c0be71b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,6 @@ pyqrcode pyinstaller pypng autopep8 +psycopg2-binary +SQLAlchemy +python-dotenv From 012e4180e0d1721f071b93df7329d7154d0811e0 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 18 Feb 2026 13:08:46 -0600 Subject: [PATCH 11/41] showuser add user database functionality linked up --- front-end/index.html | 7 +++++++ front-end/scripts/main.js | 15 +++++++++++++++ main.py | 28 +++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/front-end/index.html b/front-end/index.html index 3a3ade3..bd942c2 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -19,6 +19,13 @@ +
+
+ + + + + diff --git a/front-end/scripts/main.js b/front-end/scripts/main.js index feeebf0..133a2ba 100644 --- a/front-end/scripts/main.js +++ b/front-end/scripts/main.js @@ -6,3 +6,18 @@ function operate(operator) { document.querySelector('#output').innerText = result; }); } + +function loadUsers() { + eel.showUsers()(users => { + document.querySelector('#output').innerText = JSON.stringify(users, null, 2); + }); +} + +function addUsers() { + var email = document.querySelector('#email').value; + var password = document.querySelector('#pass').value; + + eel.addUsers(email, password)(response => { + document.querySelector('#output').innerText = response; + }); +} \ No newline at end of file diff --git a/main.py b/main.py index aae1daa..e94e512 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,44 @@ import eel +from db import engine +from sqlalchemy import text #sofia #jacob #tim eel.init('front-end') +try: + with engine.connect() as conn: + # Run a simple query to test + result = conn.execute(text("SELECT NOW();")) + print("Connected! Server time:", result.fetchone()[0]) +except Exception as e: + print("Connection failed:", e) @eel.expose def add(num1, num2): return int(num1) + int(num2) - @eel.expose def subtract(num1, num2): return int(num1) - int(num2) +@eel.expose +def showUsers(): + with engine.connect() as conn: + result = conn.execute(text("SELECT * FROM users;")) + users = result.fetchall() + return [dict(row._mapping) for row in users] + +@eel.expose +def addUsers(email, password): + #hashing logic here + + with engine.begin() as conn: # auto-commit + conn.execute( + text("INSERT INTO users (email, password_hash) VALUES (:email, :password)"), + {"email": email, "password": password} + ) + return "User added successfully" + eel.start('index.html', size=(1000, 600)) From c7beafbe103ffc9135c4d150d26a94ada9f71869 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 18 Feb 2026 13:10:59 -0600 Subject: [PATCH 12/41] password hashing --- main.py | 4 +++- requirements.txt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index e94e512..b94ee97 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import eel from db import engine from sqlalchemy import text +import bcrypt #sofia #jacob #tim @@ -32,11 +33,12 @@ def showUsers(): @eel.expose def addUsers(email, password): #hashing logic here + hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() with engine.begin() as conn: # auto-commit conn.execute( text("INSERT INTO users (email, password_hash) VALUES (:email, :password)"), - {"email": email, "password": password} + {"email": email, "password": hashed} ) return "User added successfully" diff --git a/requirements.txt b/requirements.txt index c0be71b..c5e047d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ autopep8 psycopg2-binary SQLAlchemy python-dotenv +bcrypt From a6c2e34d99e70891fb1da8401eb1c25de60e60fe Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 18 Feb 2026 17:17:14 -0600 Subject: [PATCH 13/41] Moved db queries to new html file --- front-end/db_queries.html | 32 ++++++++++++++++++++++++++++++++ front-end/index.html | 17 +++++------------ 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 front-end/db_queries.html diff --git a/front-end/db_queries.html b/front-end/db_queries.html new file mode 100644 index 0000000..f840170 --- /dev/null +++ b/front-end/db_queries.html @@ -0,0 +1,32 @@ + + + + + + + + + Hello World! + + + + + + + + + + + + +
+
+ + + + + + + + + \ No newline at end of file diff --git a/front-end/index.html b/front-end/index.html index bd942c2..112bd20 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -15,18 +15,11 @@ - - - - -
-
- - - - - - + + + + + \ No newline at end of file From 8ee5c8783a76880528b755f1490b9de30b5a09fc Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 18 Feb 2026 17:27:34 -0600 Subject: [PATCH 14/41] Initial backend commit: barebones backend inside app folder GroupFive, with model for AnalysisTask including request structure, user, language etc. Some simple tests on creation, APIView endpoint for analysis task and status endpoint. Uses serializer to interpret data from frontend --- GroupFive/__init__.py | 0 GroupFive/admin.py | 3 + GroupFive/apps.py | 6 ++ GroupFive/dummy_analysis.py | 13 +++ GroupFive/migrations/0001_initial.py | 30 +++++++ GroupFive/migrations/__init__.py | 0 GroupFive/models.py | 21 +++++ GroupFive/serializers.py | 7 ++ GroupFive/tasks.py | 22 +++++ GroupFive/tests.py | 60 +++++++++++++ GroupFive/views.py | 42 +++++++++ config/__init__.py | 0 config/asgi.py | 16 ++++ config/settings.py | 125 +++++++++++++++++++++++++++ config/urls.py | 26 ++++++ config/wsgi.py | 16 ++++ manage.py | 22 +++++ 17 files changed, 409 insertions(+) create mode 100644 GroupFive/__init__.py create mode 100644 GroupFive/admin.py create mode 100644 GroupFive/apps.py create mode 100644 GroupFive/dummy_analysis.py create mode 100644 GroupFive/migrations/0001_initial.py create mode 100644 GroupFive/migrations/__init__.py create mode 100644 GroupFive/models.py create mode 100644 GroupFive/serializers.py create mode 100644 GroupFive/tasks.py create mode 100644 GroupFive/tests.py create mode 100644 GroupFive/views.py create mode 100644 config/__init__.py create mode 100644 config/asgi.py create mode 100644 config/settings.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100644 manage.py diff --git a/GroupFive/__init__.py b/GroupFive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/GroupFive/admin.py b/GroupFive/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/GroupFive/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/GroupFive/apps.py b/GroupFive/apps.py new file mode 100644 index 0000000..8220433 --- /dev/null +++ b/GroupFive/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OurApplicationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'GroupFive' diff --git a/GroupFive/dummy_analysis.py b/GroupFive/dummy_analysis.py new file mode 100644 index 0000000..992ba89 --- /dev/null +++ b/GroupFive/dummy_analysis.py @@ -0,0 +1,13 @@ + +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/GroupFive/migrations/0001_initial.py b/GroupFive/migrations/0001_initial.py new file mode 100644 index 0000000..b8f9b01 --- /dev/null +++ b/GroupFive/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.3 on 2026-02-18 09:51 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AnalysisTask', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('input_code', models.TextField()), + ('language', models.CharField(max_length=50)), + ('status', models.CharField(choices=[('QUEUED', 'Queued'), ('RUNNING', 'Running'), ('COMPLETED', 'Completed'), ('FAILED', 'Failed')], max_length=20)), + ('results', models.JSONField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/GroupFive/migrations/__init__.py b/GroupFive/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/GroupFive/models.py b/GroupFive/models.py new file mode 100644 index 0000000..f6f0ea7 --- /dev/null +++ b/GroupFive/models.py @@ -0,0 +1,21 @@ +#all id related lines are noted and can be deleted or changed if user id is skipped or substituted +import uuid #for user ID +from django.db import models +from django.contrib.auth.models import User + +class AnalysisTask(models.Model): + #potential review request statuses + STATUS_OPT = [ + ("QUEUED", "Queued"), + ("RUNNING", "Running"), + ("COMPLETED", "Completed"), + ("FAILED", "Failed") + ] + + id = models.UUIDField(primary_key=True, default=uuid.uuid4) #more user id + user = models.ForeignKey(User, on_delete=models.CASCADE) #user id/user + input_code = models.TextField() #user provided code + language = models.CharField(max_length=50) #language of user provided code + status = models.CharField(max_length=20, choices=STATUS_OPT) #status of review request + results = models.JSONField(null=True, blank=True) #results of review + created_at = models.DateTimeField(auto_now_add=True) #creation timestamp diff --git a/GroupFive/serializers.py b/GroupFive/serializers.py new file mode 100644 index 0000000..255c15e --- /dev/null +++ b/GroupFive/serializers.py @@ -0,0 +1,7 @@ +#this file uses serializers to define what information we add to our AnalysisTask model from user +from rest_framework import serializers + +class AnalysisRequestSerializer(serializers.Serializer): + code = serializers.CharField() #for input code + #language definition of input code, can be commented out if language distinction added later + language = serializers.CharField() diff --git a/GroupFive/tasks.py b/GroupFive/tasks.py new file mode 100644 index 0000000..24c186e --- /dev/null +++ b/GroupFive/tasks.py @@ -0,0 +1,22 @@ +#from celery import shared_task #task queue to handle simultaneous requests, making testing annoying for now can readd later when necessary +from GroupFive.models import AnalysisTask +from .dummy_analysis import run_dummy + +#@shared_task --from celery, readd later +def run_analysis_async(task_id): + + #instance of analysisTask + task = AnalysisTask.objects.get(id=task_id) + task.status = "RUNNING" #update status + task.save() #save instance task + + try: + #call ai api rather than dummy + results = run_dummy(task.input_code, task.language) + + task.results = results #store results + task.status = "COMPLETED" #update status + except Exception(BaseException) as e: + task.status = "FAILED" + + task.save() \ No newline at end of file diff --git a/GroupFive/tests.py b/GroupFive/tests.py new file mode 100644 index 0000000..5f5da37 --- /dev/null +++ b/GroupFive/tests.py @@ -0,0 +1,60 @@ +from rest_framework.test import APITestCase +from django.contrib.auth.models import User +from rest_framework import status +from .models import * +from uuid import uuid4 + + +class InitialAnalysisTests(APITestCase): + + def setUp(self): + #create user + self.User = User.objects.create_user( + username="username", + password="password" + ) + self.client.login(username="username", password="password") + + def test_create_analysisTask(self): + + response = self.client.post("/api/GroupFive/",{ + "code" : "print('Hello World')", #code to analyze + "language" : "Python" #language of code + }, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("task_id", response.data) #task_id in data + self.assertEqual(response.data["status"], "QUEUED") + +class InitialWorkflowTest(APITestCase): + + def setUp(self): + #create user + self.User = User.objects.create_user( + username="username", + password="password" + ) + self.client.login(username="username", password="password") + + def test_initial_workflow(self): + response = self.client.post("/api/GroupFive/",{ + "code" : "print('Hello Again')", #code to analyze + "language" : "Python" #language of code + }, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + task_id = response.data["task_id"] + + task = AnalysisTask.objects.get(id=task_id) + + #confirm that dummy ran + self.assertEqual(task.status, "COMPLETED") + + result_response = self.client.get(f"/api/GroupFive/{task_id}") + + #ensure task endpoint + self.assertEqual(result_response.status_code, 200) + + + diff --git a/GroupFive/views.py b/GroupFive/views.py new file mode 100644 index 0000000..01cfa8e --- /dev/null +++ b/GroupFive/views.py @@ -0,0 +1,42 @@ +#all id related lines are noted and can be deleted or changed if user id is skipped or substituted +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated #for user id +from GroupFive.models import AnalysisTask +from GroupFive.serializers import AnalysisRequestSerializer +from .tasks import run_analysis_async + + +#analysis task endpoint +class AnalysisView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + serializer = AnalysisRequestSerializer(data=request.data) + serializer.is_valid(raise_exception=True) #deserialize, check correct input and format, raises 400 Bad Request on fail + + task = AnalysisTask.objects.create( + user=request.user, #user + input_code=serializer.validated_data["code"], + language=serializer.validated_data["language"], + status="QUEUED" + ) + + run_analysis_async(str(task.id)) + + return Response({ + "task_id": str(task.id), + "status": task.status + }) + +#status endpoint +class StatusView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, task_id): + task = AnalysisTask.objects.get(id=task_id, user=request.user) #user + + return Response({ + "status": task.status, + "summary": task.results if task.status == "COMPLETED" else None + }) \ 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..4a3bd85 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,125 @@ +""" +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 + +# 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 = 'django-insecure-y+j3zht6sr%!!2fg0&-ek^21&)yc+y+5a*-ly+@16$8$px)a$@' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'GroupFive', + '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': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# 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/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..a770eb4 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,26 @@ +""" +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 + +from GroupFive.views import AnalysisView + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/GroupFive/', AnalysisView.as_view(), name='GroupFive'), + path('api/GroupFive/', ) +] 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/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() From d74cf23214a4bb693bd096fff49f489f0374ae20 Mon Sep 17 00:00:00 2001 From: NathanEdwards2023 Date: Wed, 18 Feb 2026 18:29:36 -0600 Subject: [PATCH 15/41] Frontend UI --- front-end/index.html | 113 ++++++++++++++--- front-end/scripts/upload.js | 14 +++ front-end/styles/style.css | 245 ++++++++++++++++++++++++++++++++++++ 3 files changed, 357 insertions(+), 15 deletions(-) create mode 100644 front-end/scripts/upload.js diff --git a/front-end/index.html b/front-end/index.html index 3a3ade3..f5ee704 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -1,25 +1,108 @@ - - - - - Hello World! - - - + AutoPen Dashboard + + + - - - - - - - + + +
+
+

Penetration Testing Dashboard

+
System Status: Active
+
+ +
+
+

Total Scans

+

128

+
+
+

Critical Vulnerabilities

+

12

+
+
+

Medium Vulnerabilities

+

34

+
+
+

Low Vulnerabilities

+

56

+
+
+ + +
+
+

Upload Code for Analysis

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

Recent Scan Results

+ + + + + + + + + + + + + + + + + + + + + + + +
TargetDateRisk LevelStatus
example.com02/14/2026CriticalCompleted
test-server.net02/12/2026LowCompleted
+
+
+ + \ No newline at end of file diff --git a/front-end/scripts/upload.js b/front-end/scripts/upload.js new file mode 100644 index 0000000..4d33a0b --- /dev/null +++ b/front-end/scripts/upload.js @@ -0,0 +1,14 @@ +const tabButtons = document.querySelectorAll(".tab-btn"); +const tabContents = document.querySelectorAll(".tab-content"); + +tabButtons.forEach(button => { + button.addEventListener("click", () => { + // Remove active state + tabButtons.forEach(btn => btn.classList.remove("active")); + tabContents.forEach(tab => tab.classList.remove("active")); + + // Activate selected tab + button.classList.add("active"); + document.getElementById(button.dataset.tab).classList.add("active"); + }); +}); \ No newline at end of file diff --git a/front-end/styles/style.css b/front-end/styles/style.css index e69de29..8357d12 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -0,0 +1,245 @@ +/* ================= RESET ================= */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background: #000000; + color: #e5e7eb; + display: flex; + min-height: 100vh; +} + +/* ================= 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); +} + +.sidebar h2 { + color: #e9d5ff; + margin-bottom: 40px; + text-align: center; +} + +.sidebar ul { + list-style: none; +} + +.sidebar ul li { + padding: 15px; + margin: 10px 0; + background: rgba(17, 24, 39, 0.8); + border: 1px solid rgba(168, 85, 247, 0.2); + border-radius: 8px; + cursor: pointer; + transition: 0.25s; +} + +.sidebar ul li:hover { + background: rgba(17, 24, 39, 0.95); +} + +/* ================= MAIN ================= */ +.main { + flex: 1; + padding: 40px; +} + +/* ================= HEADER ================= */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 40px; +} + +.header h1 { + color: #ffffff; +} + +.status { + background: rgba(17, 24, 39, 0.8); + padding: 10px 20px; + border-radius: 999px; + border: 1px solid rgba(168, 85, 247, 0.2); +} + +/* ================= 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 { + font-size: 28px; + font-weight: bold; +} + +/* ================= 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); +} + +/* ================= SEVERITY COLORS ================= */ +.critical { color: #fb7185; font-weight: bold; } +.medium { color: #facc15; font-weight: bold; } +.low { color: #34d399; font-weight: bold; } From 5c156bb9f45fb206a5957472bdf98b7b695373f1 Mon Sep 17 00:00:00 2001 From: jacob Date: Sun, 22 Feb 2026 16:04:18 -0600 Subject: [PATCH 16/41] Renamed API folder. Fixed urls.py and api/urls.py to pass initial tests. Added database integration lacking actual database and user --- .gitignore | 2 ++ {GroupFive => api}/__init__.py | 0 {GroupFive => api}/admin.py | 0 {GroupFive => api}/apps.py | 2 +- {GroupFive => api}/dummy_analysis.py | 0 {GroupFive => api}/migrations/0001_initial.py | 0 {GroupFive => api}/migrations/__init__.py | 0 {GroupFive => api}/models.py | 0 {GroupFive => api}/serializers.py | 0 {GroupFive => api}/tasks.py | 2 +- {GroupFive => api}/tests.py | 6 +++--- {GroupFive => api}/views.py | 13 ++++++------- config/settings.py | 13 +++++++++---- config/urls.py | 9 +++++---- 14 files changed, 27 insertions(+), 20 deletions(-) rename {GroupFive => api}/__init__.py (100%) rename {GroupFive => api}/admin.py (100%) rename {GroupFive => api}/apps.py (85%) rename {GroupFive => api}/dummy_analysis.py (100%) rename {GroupFive => api}/migrations/0001_initial.py (100%) rename {GroupFive => api}/migrations/__init__.py (100%) rename {GroupFive => api}/models.py (100%) rename {GroupFive => api}/serializers.py (100%) rename {GroupFive => api}/tasks.py (94%) rename {GroupFive => api}/tests.py (89%) rename {GroupFive => api}/views.py (89%) diff --git a/.gitignore b/.gitignore index 06125d6..4a02c0a 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,8 @@ profile_default/ ipython_config.py # pyenv +#don't add database credentials +.env .python-version # pipenv diff --git a/GroupFive/__init__.py b/api/__init__.py similarity index 100% rename from GroupFive/__init__.py rename to api/__init__.py diff --git a/GroupFive/admin.py b/api/admin.py similarity index 100% rename from GroupFive/admin.py rename to api/admin.py diff --git a/GroupFive/apps.py b/api/apps.py similarity index 85% rename from GroupFive/apps.py rename to api/apps.py index 8220433..5b06c17 100644 --- a/GroupFive/apps.py +++ b/api/apps.py @@ -3,4 +3,4 @@ class OurApplicationConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'GroupFive' + name = 'api' diff --git a/GroupFive/dummy_analysis.py b/api/dummy_analysis.py similarity index 100% rename from GroupFive/dummy_analysis.py rename to api/dummy_analysis.py diff --git a/GroupFive/migrations/0001_initial.py b/api/migrations/0001_initial.py similarity index 100% rename from GroupFive/migrations/0001_initial.py rename to api/migrations/0001_initial.py diff --git a/GroupFive/migrations/__init__.py b/api/migrations/__init__.py similarity index 100% rename from GroupFive/migrations/__init__.py rename to api/migrations/__init__.py diff --git a/GroupFive/models.py b/api/models.py similarity index 100% rename from GroupFive/models.py rename to api/models.py diff --git a/GroupFive/serializers.py b/api/serializers.py similarity index 100% rename from GroupFive/serializers.py rename to api/serializers.py diff --git a/GroupFive/tasks.py b/api/tasks.py similarity index 94% rename from GroupFive/tasks.py rename to api/tasks.py index 24c186e..ddb7a07 100644 --- a/GroupFive/tasks.py +++ b/api/tasks.py @@ -1,5 +1,5 @@ #from celery import shared_task #task queue to handle simultaneous requests, making testing annoying for now can readd later when necessary -from GroupFive.models import AnalysisTask +from api.models import AnalysisTask from .dummy_analysis import run_dummy #@shared_task --from celery, readd later diff --git a/GroupFive/tests.py b/api/tests.py similarity index 89% rename from GroupFive/tests.py rename to api/tests.py index 5f5da37..06c9e67 100644 --- a/GroupFive/tests.py +++ b/api/tests.py @@ -17,7 +17,7 @@ def setUp(self): def test_create_analysisTask(self): - response = self.client.post("/api/GroupFive/",{ + response = self.client.post("/api/analysis/",{ "code" : "print('Hello World')", #code to analyze "language" : "Python" #language of code }, format="json") @@ -37,7 +37,7 @@ def setUp(self): self.client.login(username="username", password="password") def test_initial_workflow(self): - response = self.client.post("/api/GroupFive/",{ + response = self.client.post("/api/analysis/",{ "code" : "print('Hello Again')", #code to analyze "language" : "Python" #language of code }, format="json") @@ -51,7 +51,7 @@ def test_initial_workflow(self): #confirm that dummy ran self.assertEqual(task.status, "COMPLETED") - result_response = self.client.get(f"/api/GroupFive/{task_id}") + result_response = self.client.get(f"/api/analysis/{task_id}/") #ensure task endpoint self.assertEqual(result_response.status_code, 200) diff --git a/GroupFive/views.py b/api/views.py similarity index 89% rename from GroupFive/views.py rename to api/views.py index 01cfa8e..c5d1861 100644 --- a/GroupFive/views.py +++ b/api/views.py @@ -2,15 +2,14 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated #for user id -from GroupFive.models import AnalysisTask -from GroupFive.serializers import AnalysisRequestSerializer +from api.models import AnalysisTask +from api.serializers import AnalysisRequestSerializer from .tasks import run_analysis_async - -#analysis task endpoint class AnalysisView(APIView): permission_classes = [IsAuthenticated] + #analysis task endpoint def post(self, request): serializer = AnalysisRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) #deserialize, check correct input and format, raises 400 Bad Request on fail @@ -29,14 +28,14 @@ def post(self, request): "status": task.status }) -#status endpoint class StatusView(APIView): permission_classes = [IsAuthenticated] - + #status endpoint def get(self, request, task_id): task = AnalysisTask.objects.get(id=task_id, user=request.user) #user return Response({ "status": task.status, "summary": task.results if task.status == "COMPLETED" else None - }) \ No newline at end of file + }) + diff --git a/config/settings.py b/config/settings.py index 4a3bd85..be34dd9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -9,7 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.0/ref/settings/ """ - +import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -37,7 +37,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'GroupFive', + 'api', 'rest_framework' ] @@ -75,10 +75,15 @@ # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases +#modified 2/22 to implement PostgreSQL DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('DB_NAME'), + 'USER': os.getenv('DB_USER'), + 'PASSWORD': os.getenv('DB_USER'), + 'HOST': os.getenv('DB_USER'), + 'PORT': os.getenv('DB_USER'), } } diff --git a/config/urls.py b/config/urls.py index a770eb4..e0ee957 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,12 +15,13 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include -from GroupFive.views import AnalysisView +from api.views import * urlpatterns = [ path('admin/', admin.site.urls), - path('api/GroupFive/', AnalysisView.as_view(), name='GroupFive'), - path('api/GroupFive/', ) + path('api/', include('api.urls')), + + #path('/api/',) ] From ecc43b5d43787c5497482bcc59a0ab0872a70f16 Mon Sep 17 00:00:00 2001 From: jacob Date: Sun, 22 Feb 2026 16:47:33 -0600 Subject: [PATCH 17/41] api/urls.py --- api/urls.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 api/urls.py diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..5dd7cb6 --- /dev/null +++ b/api/urls.py @@ -0,0 +1,8 @@ +from django.contrib.auth import get_user +from django.urls import path +from .views import * + +urlpatterns = [ + path("analysis/", AnalysisView.as_view(), name="analysis"), + path("analysis//", StatusView.as_view(), name="analysis-status"), +] \ No newline at end of file From 80a832f53c36d64d91afb1f6bdb303c14428d60d Mon Sep 17 00:00:00 2001 From: JacobLind1 Date: Sun, 22 Feb 2026 17:23:03 -0600 Subject: [PATCH 18/41] Database Integrated with Backend, Added Requirements I had forgotten. Renamed testquery,py as it was conflicting with some python test naming. --- config/settings.py | 16 +++++++++++++--- testquery.py => db_test.py | 0 requirements.txt | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) rename testquery.py => db_test.py (100%) diff --git a/config/settings.py b/config/settings.py index be34dd9..6a40ff2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -12,6 +12,11 @@ import os from pathlib import Path +#dotenv setup otherwise import and use db.py +from dotenv import load_dotenv +load_dotenv() + + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -75,15 +80,20 @@ # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases + + #modified 2/22 to implement PostgreSQL DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.getenv('DB_NAME'), 'USER': os.getenv('DB_USER'), - 'PASSWORD': os.getenv('DB_USER'), - 'HOST': os.getenv('DB_USER'), - 'PORT': os.getenv('DB_USER'), + 'PASSWORD': os.getenv('DB_PASSWORD'), + 'HOST': os.getenv('DB_HOST'), + 'PORT': os.getenv('DB_PORT'), + 'OPTIONS': { + 'sslmode': 'require', + } } } diff --git a/testquery.py b/db_test.py similarity index 100% rename from testquery.py rename to db_test.py diff --git a/requirements.txt b/requirements.txt index c5e047d..f69e2b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ psycopg2-binary SQLAlchemy python-dotenv bcrypt +djangorestframework From 71e9bd44ce2b47036bdcfdb6a979627fe22e5d8f Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Wed, 4 Mar 2026 01:46:05 -0600 Subject: [PATCH 19/41] create the prescan function with semgrep and gitleak --- front-end/scripts/main.js | 5 +- main.py | 2 + prescan.py | 123 ++++++++++++++++++++++++++++++++++++++ prescan_report.json | 13 ++++ requirements.txt | 2 + 5 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 prescan.py create mode 100644 prescan_report.json diff --git a/front-end/scripts/main.js b/front-end/scripts/main.js index 38845f8..fc76eff 100644 --- a/front-end/scripts/main.js +++ b/front-end/scripts/main.js @@ -25,8 +25,7 @@ function askGPT() { document.querySelector('#output').innerText = "Loading..."; - eel.ask_api(prompt)(result => { + let newVar = eel.ask_api(prompt)(result => { document.querySelector('#output').innerText = result; - }); + });}} -} \ No newline at end of file diff --git a/main.py b/main.py index c97d798..1aef0c6 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,8 @@ #sofia #jacob #tim +#Nathan +#Sid eel.init('front-end') try: diff --git a/prescan.py b/prescan.py new file mode 100644 index 0000000..086cdc5 --- /dev/null +++ b/prescan.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Pre-scan 模組:對指定檔案或目錄執行 semgrep 與 gitleaks,輸出合併的 JSON 報告。 +使用方式: + python prescan.py [輸入路徑] [-o 輸出.json] + 不給路徑時預設掃描當前目錄。 +""" + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + + +def run_semgrep(target_path: str) -> dict: + """對 target_path 執行 semgrep,回傳 JSON 結果。失敗或未安裝則回傳空結構。""" + 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: + """對 target_path 執行 gitleaks detect,回傳 JSON 結果。未安裝則回傳空結構。""" + 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", "-", # 將 JSON 輸出到 stdout + ] + out = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=120, + ) + # gitleaks 找到 secret 時 exit code 可能為 1,但 stdout 仍有 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="要掃描的檔案或目錄路徑(預設: 當前目錄)", + ) + parser.add_argument( + "-o", "--output", + default="prescan_report.json", + help="輸出的 JSON 檔案路徑(預設: prescan_report.json)", + ) + args = parser.parse_args() + + input_path = os.path.normpath(args.input_path) + if not os.path.exists(input_path): + print(f"錯誤:找不到路徑 {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"已寫入: {out_path}") + + +if __name__ == "__main__": + main() diff --git a/prescan_report.json b/prescan_report.json new file mode 100644 index 0000000..63f87d9 --- /dev/null +++ b/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/requirements.txt b/requirements.txt index c5e047d..aa31d5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ eel +engine pyqrcode pyinstaller pypng @@ -7,3 +8,4 @@ psycopg2-binary SQLAlchemy python-dotenv bcrypt +semgrep From 2a92970f1d2e60fa808f34e4e8b4e3d0f35f3281 Mon Sep 17 00:00:00 2001 From: JacobLind1 Date: Wed, 4 Mar 2026 12:34:17 -0600 Subject: [PATCH 20/41] Fixed issue connecting with group database. --- config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index 6a40ff2..0dc2614 100644 --- a/config/settings.py +++ b/config/settings.py @@ -88,7 +88,7 @@ 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.getenv('DB_NAME'), 'USER': os.getenv('DB_USER'), - 'PASSWORD': os.getenv('DB_PASSWORD'), + 'PASSWORD': os.getenv('DB_PASS'), 'HOST': os.getenv('DB_HOST'), 'PORT': os.getenv('DB_PORT'), 'OPTIONS': { From 80af157e75c5a96c84a4a270d75ef57eb160b817 Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Wed, 4 Mar 2026 13:05:01 -0600 Subject: [PATCH 21/41] create the prescan function with semgrep and gitleak --- prescan.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/prescan.py b/prescan.py index 086cdc5..e6d51ca 100644 --- a/prescan.py +++ b/prescan.py @@ -1,11 +1,4 @@ #!/usr/bin/env python3 -""" -Pre-scan 模組:對指定檔案或目錄執行 semgrep 與 gitleaks,輸出合併的 JSON 報告。 -使用方式: - python prescan.py [輸入路徑] [-o 輸出.json] - 不給路徑時預設掃描當前目錄。 -""" - import argparse import json import os @@ -15,7 +8,7 @@ def run_semgrep(target_path: str) -> dict: - """對 target_path 執行 semgrep,回傳 JSON 結果。失敗或未安裝則回傳空結構。""" + path = Path(target_path).resolve() if not path.exists(): return {"tool": "semgrep", "error": f"path not found: {target_path}", "results": []} @@ -51,7 +44,7 @@ def run_semgrep(target_path: str) -> dict: def run_gitleaks(target_path: str) -> dict: - """對 target_path 執行 gitleaks detect,回傳 JSON 結果。未安裝則回傳空結構。""" + """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": []} @@ -62,7 +55,7 @@ def run_gitleaks(target_path: str) -> dict: "--source", source, "--no-git", "--report-format", "json", - "--report-path", "-", # 將 JSON 輸出到 stdout + "--report-path", "-", # write JSON to stdout ] out = subprocess.run( cmd, @@ -70,7 +63,7 @@ def run_gitleaks(target_path: str) -> dict: text=True, timeout=120, ) - # gitleaks 找到 secret 時 exit code 可能為 1,但 stdout 仍有 JSON + # 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": []} @@ -92,18 +85,18 @@ def main(): "input_path", nargs="?", default=".", - help="要掃描的檔案或目錄路徑(預設: 當前目錄)", + help="File or directory path to scan (default: current directory)", ) parser.add_argument( "-o", "--output", default="prescan_report.json", - help="輸出的 JSON 檔案路徑(預設: 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"錯誤:找不到路徑 {input_path}", file=sys.stderr) + print(f"Error: path not found {input_path}", file=sys.stderr) sys.exit(1) report = { @@ -116,7 +109,7 @@ def main(): with open(out_path, "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2) - print(f"已寫入: {out_path}") + print(f"Report written to: {out_path}") if __name__ == "__main__": From 5db65b6ea526942b848d324875f72287c8d1e2a4 Mon Sep 17 00:00:00 2001 From: NathanEdwards2023 Date: Wed, 4 Mar 2026 21:22:28 -0600 Subject: [PATCH 22/41] Simple Login and Register pages. Needs to be linked to backend --- front-end/login.html | 30 ++++++++++++++++++++++++++++++ front-end/register.html | 27 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 front-end/login.html create mode 100644 front-end/register.html diff --git a/front-end/login.html b/front-end/login.html new file mode 100644 index 0000000..425f5a7 --- /dev/null +++ b/front-end/login.html @@ -0,0 +1,30 @@ + + + + Login + + + +

Login

+ +{% if error %} +

{{ error }}

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

+ + +

+ + +
+ +

Don't have an account?

+Register + + + \ No newline at end of file diff --git a/front-end/register.html b/front-end/register.html new file mode 100644 index 0000000..18bf32c --- /dev/null +++ b/front-end/register.html @@ -0,0 +1,27 @@ + + + + Register + + + +

Create Account

+ +
+ {% csrf_token %} + + +

+ + +

+ + + +
+ +
+Back to Login + + + \ No newline at end of file From c351af70ea5ca7f1696b862ef0869a723a560476 Mon Sep 17 00:00:00 2001 From: Sidh05 Date: Thu, 12 Mar 2026 17:41:17 -0500 Subject: [PATCH 23/41] Test cases- Safe, vulnerable, mixed and edge --- test_cases/edge1.py | 16 ++++++++++++++++ test_cases/edhe2.js | 3 +++ test_cases/mixed1.py | 24 ++++++++++++++++++++++++ test_cases/mixed2.js | 11 +++++++++++ test_cases/safe1.py | 7 +++++++ test_cases/safe2.py | 6 ++++++ test_cases/safe3.py | 6 ++++++ test_cases/safe4.c | 10 ++++++++++ test_cases/vulnerable1.py | 14 ++++++++++++++ test_cases/vulnerable2.py | 8 ++++++++ test_cases/vulnerable3.c | 7 +++++++ test_cases/vulnerable4.js | 7 +++++++ 12 files changed, 119 insertions(+) create mode 100644 test_cases/edge1.py create mode 100644 test_cases/edhe2.js create mode 100644 test_cases/mixed1.py create mode 100644 test_cases/mixed2.js create mode 100644 test_cases/safe1.py create mode 100644 test_cases/safe2.py create mode 100644 test_cases/safe3.py create mode 100644 test_cases/safe4.c create mode 100644 test_cases/vulnerable1.py create mode 100644 test_cases/vulnerable2.py create mode 100644 test_cases/vulnerable3.c create mode 100644 test_cases/vulnerable4.js 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 From 57cde04e7f5c9d34d0c35c7c5ad1367496a05565 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Thu, 19 Mar 2026 14:28:15 -0500 Subject: [PATCH 24/41] Refactored everything in main branch - changed group5 into a new app called scanner - updated everything to fully remove eel functionality - cleaned up duplicate code and unneeded testing code --- GroupFive/migrations/0001_initial.py | 30 ------- GroupFive/models.py | 21 ----- GroupFive/serializers.py | 7 -- GroupFive/tasks.py | 22 ----- GroupFive/tests.py | 60 -------------- GroupFive/views.py | 42 ---------- config/settings.py | 36 ++++++--- config/urls.py | 10 +-- db.py | 21 ----- front-end/db_queries.html | 32 -------- front-end/index.html | 21 +++-- front-end/scripts/main.js | 32 -------- main.py | 70 ---------------- requirements.txt | 13 +-- {GroupFive => scanner}/__init__.py | 0 {GroupFive => scanner}/admin.py | 0 {GroupFive => scanner}/apps.py | 4 +- scanner/migrations/0001_initial.py | 77 ++++++++++++++++++ {GroupFive => scanner}/migrations/__init__.py | 0 scanner/models.py | 81 +++++++++++++++++++ scanner/serializers.py | 26 ++++++ scanner/services/ai_service.py | 13 +++ .../services}/dummy_analysis.py | 1 - scanner/services/user_services.py | 9 +++ scanner/tasks.py | 43 ++++++++++ scanner/tests.py | 58 +++++++++++++ scanner/urls.py | 10 +++ scanner/views.py | 68 ++++++++++++++++ testquery.py | 15 ---- 29 files changed, 431 insertions(+), 391 deletions(-) delete mode 100644 GroupFive/migrations/0001_initial.py delete mode 100644 GroupFive/models.py delete mode 100644 GroupFive/serializers.py delete mode 100644 GroupFive/tasks.py delete mode 100644 GroupFive/tests.py delete mode 100644 GroupFive/views.py delete mode 100644 db.py delete mode 100644 front-end/db_queries.html delete mode 100644 front-end/scripts/main.js delete mode 100644 main.py rename {GroupFive => scanner}/__init__.py (100%) rename {GroupFive => scanner}/admin.py (100%) rename {GroupFive => scanner}/apps.py (60%) create mode 100644 scanner/migrations/0001_initial.py rename {GroupFive => scanner}/migrations/__init__.py (100%) create mode 100644 scanner/models.py create mode 100644 scanner/serializers.py create mode 100644 scanner/services/ai_service.py rename {GroupFive => scanner/services}/dummy_analysis.py (99%) create mode 100644 scanner/services/user_services.py create mode 100644 scanner/tasks.py create mode 100644 scanner/tests.py create mode 100644 scanner/urls.py create mode 100644 scanner/views.py delete mode 100644 testquery.py diff --git a/GroupFive/migrations/0001_initial.py b/GroupFive/migrations/0001_initial.py deleted file mode 100644 index b8f9b01..0000000 --- a/GroupFive/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.0.3 on 2026-02-18 09:51 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AnalysisTask', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('input_code', models.TextField()), - ('language', models.CharField(max_length=50)), - ('status', models.CharField(choices=[('QUEUED', 'Queued'), ('RUNNING', 'Running'), ('COMPLETED', 'Completed'), ('FAILED', 'Failed')], max_length=20)), - ('results', models.JSONField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/GroupFive/models.py b/GroupFive/models.py deleted file mode 100644 index f6f0ea7..0000000 --- a/GroupFive/models.py +++ /dev/null @@ -1,21 +0,0 @@ -#all id related lines are noted and can be deleted or changed if user id is skipped or substituted -import uuid #for user ID -from django.db import models -from django.contrib.auth.models import User - -class AnalysisTask(models.Model): - #potential review request statuses - STATUS_OPT = [ - ("QUEUED", "Queued"), - ("RUNNING", "Running"), - ("COMPLETED", "Completed"), - ("FAILED", "Failed") - ] - - id = models.UUIDField(primary_key=True, default=uuid.uuid4) #more user id - user = models.ForeignKey(User, on_delete=models.CASCADE) #user id/user - input_code = models.TextField() #user provided code - language = models.CharField(max_length=50) #language of user provided code - status = models.CharField(max_length=20, choices=STATUS_OPT) #status of review request - results = models.JSONField(null=True, blank=True) #results of review - created_at = models.DateTimeField(auto_now_add=True) #creation timestamp diff --git a/GroupFive/serializers.py b/GroupFive/serializers.py deleted file mode 100644 index 255c15e..0000000 --- a/GroupFive/serializers.py +++ /dev/null @@ -1,7 +0,0 @@ -#this file uses serializers to define what information we add to our AnalysisTask model from user -from rest_framework import serializers - -class AnalysisRequestSerializer(serializers.Serializer): - code = serializers.CharField() #for input code - #language definition of input code, can be commented out if language distinction added later - language = serializers.CharField() diff --git a/GroupFive/tasks.py b/GroupFive/tasks.py deleted file mode 100644 index 24c186e..0000000 --- a/GroupFive/tasks.py +++ /dev/null @@ -1,22 +0,0 @@ -#from celery import shared_task #task queue to handle simultaneous requests, making testing annoying for now can readd later when necessary -from GroupFive.models import AnalysisTask -from .dummy_analysis import run_dummy - -#@shared_task --from celery, readd later -def run_analysis_async(task_id): - - #instance of analysisTask - task = AnalysisTask.objects.get(id=task_id) - task.status = "RUNNING" #update status - task.save() #save instance task - - try: - #call ai api rather than dummy - results = run_dummy(task.input_code, task.language) - - task.results = results #store results - task.status = "COMPLETED" #update status - except Exception(BaseException) as e: - task.status = "FAILED" - - task.save() \ No newline at end of file diff --git a/GroupFive/tests.py b/GroupFive/tests.py deleted file mode 100644 index 5f5da37..0000000 --- a/GroupFive/tests.py +++ /dev/null @@ -1,60 +0,0 @@ -from rest_framework.test import APITestCase -from django.contrib.auth.models import User -from rest_framework import status -from .models import * -from uuid import uuid4 - - -class InitialAnalysisTests(APITestCase): - - def setUp(self): - #create user - self.User = User.objects.create_user( - username="username", - password="password" - ) - self.client.login(username="username", password="password") - - def test_create_analysisTask(self): - - response = self.client.post("/api/GroupFive/",{ - "code" : "print('Hello World')", #code to analyze - "language" : "Python" #language of code - }, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("task_id", response.data) #task_id in data - self.assertEqual(response.data["status"], "QUEUED") - -class InitialWorkflowTest(APITestCase): - - def setUp(self): - #create user - self.User = User.objects.create_user( - username="username", - password="password" - ) - self.client.login(username="username", password="password") - - def test_initial_workflow(self): - response = self.client.post("/api/GroupFive/",{ - "code" : "print('Hello Again')", #code to analyze - "language" : "Python" #language of code - }, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - task_id = response.data["task_id"] - - task = AnalysisTask.objects.get(id=task_id) - - #confirm that dummy ran - self.assertEqual(task.status, "COMPLETED") - - result_response = self.client.get(f"/api/GroupFive/{task_id}") - - #ensure task endpoint - self.assertEqual(result_response.status_code, 200) - - - diff --git a/GroupFive/views.py b/GroupFive/views.py deleted file mode 100644 index 01cfa8e..0000000 --- a/GroupFive/views.py +++ /dev/null @@ -1,42 +0,0 @@ -#all id related lines are noted and can be deleted or changed if user id is skipped or substituted -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated #for user id -from GroupFive.models import AnalysisTask -from GroupFive.serializers import AnalysisRequestSerializer -from .tasks import run_analysis_async - - -#analysis task endpoint -class AnalysisView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request): - serializer = AnalysisRequestSerializer(data=request.data) - serializer.is_valid(raise_exception=True) #deserialize, check correct input and format, raises 400 Bad Request on fail - - task = AnalysisTask.objects.create( - user=request.user, #user - input_code=serializer.validated_data["code"], - language=serializer.validated_data["language"], - status="QUEUED" - ) - - run_analysis_async(str(task.id)) - - return Response({ - "task_id": str(task.id), - "status": task.status - }) - -#status endpoint -class StatusView(APIView): - permission_classes = [IsAuthenticated] - - def get(self, request, task_id): - task = AnalysisTask.objects.get(id=task_id, user=request.user) #user - - return Response({ - "status": task.status, - "summary": task.results if task.status == "COMPLETED" else None - }) \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index 4a3bd85..dd62635 100644 --- a/config/settings.py +++ b/config/settings.py @@ -11,6 +11,8 @@ """ 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 @@ -20,7 +22,7 @@ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-y+j3zht6sr%!!2fg0&-ek^21&)yc+y+5a*-ly+@16$8$px)a$@' +SECRET_KEY = config('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -37,7 +39,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'GroupFive', + 'scanner', 'rest_framework' ] @@ -56,7 +58,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'front-end')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -75,13 +77,6 @@ # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - # Password validation # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators @@ -117,9 +112,28 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = 'static/' +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 index a770eb4..197c1e1 100644 --- a/config/urls.py +++ b/config/urls.py @@ -14,13 +14,13 @@ 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 -from GroupFive.views import AnalysisView +from django.contrib import admin +from django.urls import path, include +from django.views.generic import TemplateView urlpatterns = [ path('admin/', admin.site.urls), - path('api/GroupFive/', AnalysisView.as_view(), name='GroupFive'), - path('api/GroupFive/', ) + path('api/scanner/', include('scanner.urls')), # include scanner app URLs + path('', TemplateView.as_view(template_name="index.html")), ] diff --git a/db.py b/db.py deleted file mode 100644 index 5afc09b..0000000 --- a/db.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from dotenv import load_dotenv - -load_dotenv() - -DB_URL = f"postgresql://" \ - f"{os.getenv('DB_USER')}:" \ - f"{os.getenv('DB_PASS')}@" \ - f"{os.getenv('DB_HOST')}:" \ - f"{os.getenv('DB_PORT')}/" \ - f"{os.getenv('DB_NAME')}" - -engine = create_engine( - DB_URL, - echo=True, - connect_args={"sslmode": "require"} -) - -SessionLocal = sessionmaker(bind=engine) \ No newline at end of file diff --git a/front-end/db_queries.html b/front-end/db_queries.html deleted file mode 100644 index f840170..0000000 --- a/front-end/db_queries.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - Hello World! - - - - - - - - - - - - -
-
- - - - - - - - - \ No newline at end of file diff --git a/front-end/index.html b/front-end/index.html index f824cb1..0db3cde 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -1,22 +1,21 @@ +{% load static %} - AutoPen Dashboard - - - - - Hello World! - - - + + AutoPen Dashboard + + + + + + - - + - -

Recent Scan Results

diff --git a/front-end/scripts/upload.js b/front-end/scripts/upload.js index 4d33a0b..4f27024 100644 --- a/front-end/scripts/upload.js +++ b/front-end/scripts/upload.js @@ -1,14 +1,16 @@ -const tabButtons = document.querySelectorAll(".tab-btn"); -const tabContents = document.querySelectorAll(".tab-content"); +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")); + 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"); + // Activate selected tab + button.classList.add("active"); + document.getElementById(button.dataset.tab).classList.add("active"); + }); }); }); \ No newline at end of file From bc332c28354d4a267c78916873f62b75577d4270 Mon Sep 17 00:00:00 2001 From: kaye-s Date: Wed, 1 Apr 2026 17:18:22 -0500 Subject: [PATCH 27/41] Logout button --- api/urls.py | 3 +- api/views.py | 16 ++++++++--- config/settings.py | 3 +- db_test.py | 0 front-end/index.html | 57 ++++++++++++++++++++++++++------------ front-end/login.html | 44 ++++++++++++++++++----------- front-end/styles/style.css | 5 +++- main.py | 0 8 files changed, 87 insertions(+), 41 deletions(-) delete mode 100644 db_test.py delete mode 100644 main.py diff --git a/api/urls.py b/api/urls.py index c9545ec..90bd453 100644 --- a/api/urls.py +++ b/api/urls.py @@ -10,8 +10,7 @@ # Dashboard (default root) path('', views.dashboard_view, name='dashboard'), - # Create a new code submission and run analysis - path('submit-code/', SubmissionView.as_view(), name='create_submission'), + path('submit/', views.submit_code, name='submit_code'), # Check status / get results of a submission path('submission//', SubmissionStatusView.as_view(), name='submission_status'), diff --git a/api/views.py b/api/views.py index cbce02a..7ea075b 100644 --- a/api/views.py +++ b/api/views.py @@ -110,9 +110,17 @@ def dashboard_view(request): # ------------------- @login_required def submit_code(request): + print("HIT SUBMIT VIEW") result = None + if request.method == "POST": - code = request.POST.get("code") - # For demo, just return a dummy response - result = f"Received {len(code.splitlines())} lines of code. Dummy analysis: All good!" - return render(request, 'submit_code.html', {'result': result}) + code = request.POST.get("code", "") + + print("CODE:", code) # debug + + if not code.strip(): + result = "No code submitted." + else: + result = f"Received {len(code.splitlines())} lines of code. Dummy analysis: All good!" + + return render(request, 'index.html', {'result': result}) diff --git a/config/settings.py b/config/settings.py index afae9e2..d1fcc39 100644 --- a/config/settings.py +++ b/config/settings.py @@ -29,7 +29,8 @@ ALLOWED_HOSTS = [] -LOGIN_URL = '/login/' +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'dashboard' # Application definition diff --git a/db_test.py b/db_test.py deleted file mode 100644 index e69de29..0000000 diff --git a/front-end/index.html b/front-end/index.html index 0a65a70..3386def 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -31,7 +31,21 @@

AutoPen

Penetration Testing Dashboard

+

User: {{ request.user }}

System Status: Active
+
+ {% csrf_token %} + +
@@ -53,30 +67,39 @@

Low Vulnerabilities

-

Upload Code for Analysis

-
- - -
+
+ {% csrf_token %} - -
- -
+
+ + +
- -
- -
+ +
+ +
- + +
+ +
+ + + + + {% if result %} +
+ {{ result }} +
+ {% endif %}
diff --git a/front-end/login.html b/front-end/login.html index 425f5a7..06080e9 100644 --- a/front-end/login.html +++ b/front-end/login.html @@ -1,30 +1,42 @@ +{% load static %} - Login + Login - AutoPen + - -

Login

+ -{% if error %} -

{{ error }}

-{% endif %} +
+
-
- {% csrf_token %} +

AutoPen Login

- -

+ {% if error %} +

{{ error }}

+ {% endif %} - -

+ + {% csrf_token %} - - + -

Don't have an account?

-Register + + + + + +

+ Don't have an account? +

+ +
+ Register +
+ +
+
\ No newline at end of file diff --git a/front-end/styles/style.css b/front-end/styles/style.css index 8357d12..08fe5e3 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -73,7 +73,10 @@ body { /* ================= UPLOAD SECTION ================= */ .code-center { - margin-bottom: 50px; + display: flex; + justify-content: center; /* horizontal center */ + align-items: center; /* vertical center */ + min-height: 100vh; /* full screen height */ } .code-box { diff --git a/main.py b/main.py deleted file mode 100644 index e69de29..0000000 From 797a4e1e4c26e14c203f9bfbce2274d3c40da6d8 Mon Sep 17 00:00:00 2001 From: NathanEdwards2023 Date: Wed, 1 Apr 2026 18:31:02 -0500 Subject: [PATCH 28/41] UX Updates from previous branch --- front-end/index.html | 34 ++++--- front-end/login.html | 47 +++++----- front-end/scripts/login.js | 33 +++++++ front-end/scripts/main.js | 18 ++++ front-end/styles/login.css | 148 +++++++++++++++++++++++++++++++ front-end/styles/style.css | 175 ++++++++++++++++++++++++++++++++----- 6 files changed, 394 insertions(+), 61 deletions(-) create mode 100644 front-end/scripts/login.js create mode 100644 front-end/scripts/main.js create mode 100644 front-end/styles/login.css diff --git a/front-end/index.html b/front-end/index.html index db2c5f7..0af93ca 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -10,12 +10,20 @@ + + + +
+ + + +

Penetration Testing Dashboard

-

User: {{ request.user }}

System Status: Active
-
- {% csrf_token %} - -
diff --git a/front-end/login.html b/front-end/login.html index 06080e9..a7d2d34 100644 --- a/front-end/login.html +++ b/front-end/login.html @@ -1,42 +1,43 @@ {% load static %} - + Login - AutoPen - + - + -
-
+ diff --git a/front-end/styles/style.css b/front-end/styles/style.css index 08fe5e3..8f76752 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -243,6 +243,15 @@ tr:hover { } /* ================= SEVERITY COLORS ================= */ -.critical { color: #fb7185; font-weight: bold; } -.medium { color: #facc15; font-weight: bold; } -.low { color: #34d399; font-weight: bold; } +.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; +} \ No newline at end of file diff --git a/front-end/vulnerabilities.html b/front-end/vulnerabilities.html new file mode 100644 index 0000000..c3a5e68 --- /dev/null +++ b/front-end/vulnerabilities.html @@ -0,0 +1,101 @@ + +{% load static %} + + + Vulnerabilities + + + + + + + + + +
+ + +
+

Vulnerability Database

+
Live Data
+
+ + +
+

Search & Filter

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

All Vulnerabilities

+ +
+ + + + + + + + + + + + + {% for v in vulnerabilities %} + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
CWENameCategoryCVSSScoreSeverity
{{ v.cwe_id }} + {{ v.name }} + {{ v.categories }}{{ v.cvss_version }}{{ v.average_score }} + {{ v.severity }} +
No vulnerabilities found.
+
+ + + + + \ No newline at end of file From d2d721d74cb8a36b6d4ed2e75026ce00ffbd71a6 Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Wed, 8 Apr 2026 21:37:36 -0500 Subject: [PATCH 33/41] AI report --- api/services/incident_report_ai.py | 69 +++++++++ api/utils/incident_report.py | 142 +++++++++++++++++++ api/views.py | 146 ++++++++++--------- front-end/incident_report.html | 203 ++++++++++++++++++++++++++ front-end/styles/incident_report.css | 204 +++++++++++++++++++++++++++ 5 files changed, 695 insertions(+), 69 deletions(-) create mode 100644 api/services/incident_report_ai.py create mode 100644 api/utils/incident_report.py create mode 100644 front-end/incident_report.html create mode 100644 front-end/styles/incident_report.css diff --git a/api/services/incident_report_ai.py b/api/services/incident_report_ai.py new file mode 100644 index 0000000..8eafb6e --- /dev/null +++ b/api/services/incident_report_ai.py @@ -0,0 +1,69 @@ +""" +OpenAI call for structured incident report JSON (template is rendered server-side). +""" +from __future__ import annotations + +import json +from typing import Any + +from openai import OpenAI +from decouple import config + +client = OpenAI(api_key=config("OPENAI_API_KEY")) + +# System message: enforce JSON-only output for downstream parsing. +_SYSTEM = ( + "You are a senior cybersecurity analyst. " + "You must respond with a single valid JSON object only (no markdown, no prose outside JSON). " + "Base every field strictly on the provided passage (user code + tool outputs). " + "If information is missing, use concise placeholders like \"N/A\" or \"Not evidenced in scan data\". " + "Do not fabricate CVEs, exploits, or incidents not supported by the passage." +) + + +def build_incident_report_user_prompt(passage: dict[str, Any]) -> str: + passage_json = json.dumps(passage, ensure_ascii=False, indent=2) + schema = """ +Return a JSON object with exactly these keys (all string values unless noted): + +- severity_level: short label (e.g. "Low", "Medium", "High", "Critical", or "Informational") +- incident_type: short type based on findings (e.g. "Secret exposure", "Injection risk", "Misconfiguration") +- systems_affected: what is at risk in plain language (the analyzed code context) +- discovery_method: how issues were found (mention semgrep/gitleaks only if present in passage) +- status: short status string suitable for an executive summary table +- cvss: object with keys base, threat, environmental, supplemental — each a string score "0.0"-"10.0" or "N/A" +- what_happened: 2-5 sentences describing the situation for a non-technical reader +- impact: array of strings; each item one concrete impact statement +- follow_up_consequences: 2-4 sentences on consequences if the organization follows up on recommendations +- no_follow_up_consequences: 2-4 sentences on consequences if recommendations are not followed +- response_actions: array of objects { "action": string, "details": string } with 4-8 practical remediation steps + +Also incorporate this analytical requirement into the narrative fields (what_happened, impact, follow_up_consequences, no_follow_up_consequences): +Analyze this passage and tell me the consequences of following up and not following up. + +Passage (JSON): +""" + return schema.strip() + "\n" + passage_json + + +def generate_incident_report_ai_payload(passage: dict[str, Any]) -> str: + """ + Calls the chat completion API and returns raw message content (expected JSON object). + """ + user_prompt = build_incident_report_user_prompt(passage) + kwargs: dict[str, Any] = { + "model": "gpt-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) + + return (resp.choices[0].message.content or "").strip() diff --git a/api/utils/incident_report.py b/api/utils/incident_report.py new file mode 100644 index 0000000..de378bb --- /dev/null +++ b/api/utils/incident_report.py @@ -0,0 +1,142 @@ +""" +Helpers to parse LLM JSON output and merge backend-filled fields for incident reports. +""" +from __future__ import annotations + +import json +import re +import uuid +from typing import Any + +from django.utils import timezone + + +def parse_llm_json(raw: str) -> dict[str, Any]: + """ + Parse JSON from model output. Strips optional ```json ... ``` fences. + """ + text = (raw or "").strip() + if not text: + return {} + + fence = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE) + if fence: + text = fence.group(1).strip() + + try: + data = json.loads(text) + return data if isinstance(data, dict) else {} + except json.JSONDecodeError: + return {} + + +def _as_str(value: Any, default: str = "") -> str: + if value is None: + return default + if isinstance(value, str): + return value + return str(value) + + +def _as_str_list(value: Any) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return [value] if value.strip() else [] + if isinstance(value, list): + out: list[str] = [] + for item in value: + s = _as_str(item).strip() + if s: + out.append(s) + return out + return [] + + +def _normalize_cvss(ai: dict[str, Any]) -> dict[str, str]: + raw = ai.get("cvss") + if not isinstance(raw, dict): + raw = {} + return { + "base": _as_str(raw.get("base"), "N/A"), + "threat": _as_str(raw.get("threat"), "N/A"), + "environmental": _as_str(raw.get("environmental"), "N/A"), + "supplemental": _as_str(raw.get("supplemental"), "N/A"), + } + + +def _normalize_response_actions(ai: dict[str, Any]) -> list[dict[str, str]]: + rows = ai.get("response_actions") + if not isinstance(rows, list): + return [] + out: list[dict[str, str]] = [] + for row in rows: + if not isinstance(row, dict): + continue + action = _as_str(row.get("action")).strip() + details = _as_str(row.get("details")).strip() + if action or details: + out.append({"action": action or "—", "details": details or "—"}) + return out + + +def merge_incident_report_context( + *, + request, + ai: dict[str, Any], + parse_error: str | None = None, +) -> dict[str, Any]: + """ + Backend-owned fields + normalized AI fields for template rendering. + """ + now = timezone.now() + incident_id = f"CYB-{now.year}-{uuid.uuid4().hex[:4].upper()}" + dt_display = now.strftime("%d %B %Y, %H:%M %Z").strip() or now.isoformat() + + user = getattr(request, "user", None) + if user is not None and getattr(user, "is_authenticated", False): + reported_by = user.get_username() or _as_str(getattr(user, "email", ""), "Unknown") + else: + reported_by = "Unknown" + + cvss = _normalize_cvss(ai) + impact = _as_str_list(ai.get("impact")) + response_actions = _normalize_response_actions(ai) + + return { + "report_kicker": "Cybersecurity", + "report_title": "Incident Report", + "report_subtitle": "CODE SECURITY ANALYSIS", + "incident_id": incident_id, + "report_datetime": dt_display, + "reported_by": reported_by, + "severity_level": _as_str(ai.get("severity_level"), "Unknown"), + "incident_type": _as_str(ai.get("incident_type"), "Code security review"), + "systems_affected": _as_str(ai.get("systems_affected"), "Submitted code artifact"), + "discovery_method": _as_str( + ai.get("discovery_method"), + "Automated static analysis (semgrep) and secret scanning (gitleaks)", + ), + "status": _as_str(ai.get("status"), "Analysis complete"), + "cvss_base": cvss["base"], + "cvss_threat": cvss["threat"], + "cvss_environmental": cvss["environmental"], + "cvss_supplemental": cvss["supplemental"], + "what_happened": _as_str( + ai.get("what_happened"), + "No narrative could be generated from the model output.", + ), + "impact_items": impact, + "follow_up_consequences": _as_str(ai.get("follow_up_consequences"), ""), + "no_follow_up_consequences": _as_str(ai.get("no_follow_up_consequences"), ""), + "response_actions": response_actions, + "parse_error": parse_error or "", + } + + +DISCLAIMER_TEXT = ( + "This report is partially generated with the assistance of automated code pre-processing " + "and OpenAI-based report generation. It should not be treated as a substitute for manual " + "security review, professional judgment, or formal penetration testing validation. Any " + "findings, risk ratings, and recommendations should be independently verified before use." +) diff --git a/api/views.py b/api/views.py index aab21d0..c7fd554 100644 --- a/api/views.py +++ b/api/views.py @@ -7,6 +7,12 @@ from django.http import JsonResponse import json from .services.ai_service import ask_ai +from .services.incident_report_ai import generate_incident_report_ai_payload +from .utils.incident_report import ( + DISCLAIMER_TEXT, + merge_incident_report_context, + parse_llm_json, +) from django.contrib.auth.decorators import login_required from django.contrib.auth import authenticate, login, logout from .models import CodeSubmission, File, Threat @@ -114,72 +120,74 @@ def dashboard_view(request): # ------------------- @login_required def submit_code(request): - print("HIT SUBMIT VIEW") - result = None - - if request.method == "POST": - code = request.POST.get("code", "") - - print("CODE:", code) # debug - - if not code.strip(): - result = "No code submitted." - else: - # Create an isolated temp directory so semgrep/gitleaks can scan it safely. - tmp_dir_path = Path(tempfile.mkdtemp(prefix="autopen_")) - try: - target_file_path = tmp_dir_path / "target.py" - target_file_path.write_text(code, encoding="utf-8") - - # Pre-process: run semgrep + gitleaks against the temporary file. - semgrep_report = run_semgrep(str(target_file_path)) - gitleaks_report = run_gitleaks(str(target_file_path)) - - # Keep the prompt size bounded. - max_code_chars = 8000 - truncated = code[:max_code_chars] - trunc_note = "" - if len(code) > max_code_chars: - trunc_note = f"\n\n[NOTE] Code was truncated to the first {max_code_chars} characters." - - # Keep tool findings compact to reduce token usage. - semgrep_results = semgrep_report.get("results", []) or [] - gitleaks_results = gitleaks_report.get("results", []) or [] - semgrep_results = semgrep_results[:20] - gitleaks_results = gitleaks_results[:20] - - # Assemble a single "passage" for OpenAI analysis. - passage = { - "user_code": truncated + trunc_note, - "semgrep": { - "error": semgrep_report.get("error"), - "results": semgrep_results, - }, - "gitleaks": { - "error": gitleaks_report.get("error"), - "results": gitleaks_results, - }, - } - - prompt = ( - "You are a security analysis assistant.\n\n" - "Analyze this passage and tell me the consequences of following up and not following up.\n\n" - "## Passage (code + tool findings)\n" - f"{json.dumps(passage, ensure_ascii=False, indent=2)}\n\n" - "## Output requirements\n" - "1. Provide a clear overall risk summary (1-2 sentences).\n" - "2. Provide 'Consequences if following up' and 'Consequences if not following up'.\n" - "3. For each consequence, include which tool finding(s) it is based on.\n" - "4. End with 'Recommended next steps' (3-5 bullet points) focusing on practical remediation.\n" - "5. Do not invent findings that are not supported by the passage.\n" - ) - - result = ask_ai(prompt) - except Exception as e: - # Return an error string so the front-end still shows something useful. - result = f"Analysis failed: {type(e).__name__}: {e}" - finally: - # Best-effort cleanup of temporary files. - shutil.rmtree(tmp_dir_path, ignore_errors=True) - - return render(request, 'index.html', {'result': result}) + if request.method != "POST": + return redirect("dashboard") + + code = request.POST.get("code", "").strip() + if not code: + return render( + request, + "index.html", + {"result": "No code submitted."}, + ) + + # Create an isolated temp directory so semgrep/gitleaks can scan it safely. + tmp_dir_path = Path(tempfile.mkdtemp(prefix="autopen_")) + try: + target_file_path = tmp_dir_path / "target.py" + target_file_path.write_text(code, encoding="utf-8") + + # Pre-process: run semgrep + gitleaks against the temporary file. + semgrep_report = run_semgrep(str(target_file_path)) + gitleaks_report = run_gitleaks(str(target_file_path)) + + # Keep the prompt size bounded. + max_code_chars = 8000 + truncated = code[:max_code_chars] + trunc_note = "" + if len(code) > max_code_chars: + trunc_note = f"\n\n[NOTE] Code was truncated to the first {max_code_chars} characters." + + # Keep tool findings compact to reduce token usage. + semgrep_results = semgrep_report.get("results", []) or [] + gitleaks_results = gitleaks_report.get("results", []) or [] + semgrep_results = semgrep_results[:20] + gitleaks_results = gitleaks_results[:20] + + # Assemble a single "passage" for OpenAI analysis. + passage = { + "user_code": truncated + trunc_note, + "semgrep": { + "error": semgrep_report.get("error"), + "results": semgrep_results, + }, + "gitleaks": { + "error": gitleaks_report.get("error"), + "results": gitleaks_results, + }, + } + + raw_json = generate_incident_report_ai_payload(passage) + ai_data = parse_llm_json(raw_json) + parse_error = None + if not ai_data: + parse_error = ( + "The model returned empty or non-JSON output; placeholder values are shown where needed." + ) + + ctx = merge_incident_report_context( + request=request, + ai=ai_data, + parse_error=parse_error, + ) + ctx["disclaimer"] = DISCLAIMER_TEXT + return render(request, "incident_report.html", ctx) + except Exception as e: + return render( + request, + "index.html", + {"result": f"Analysis failed: {type(e).__name__}: {e}"}, + ) + finally: + # Best-effort cleanup of temporary files. + shutil.rmtree(tmp_dir_path, ignore_errors=True) diff --git a/front-end/incident_report.html b/front-end/incident_report.html new file mode 100644 index 0000000..6eff57a --- /dev/null +++ b/front-end/incident_report.html @@ -0,0 +1,203 @@ + +{% load static %} + + + + + Incident Report — AutoPen + + + + +
+
+ ← Back to dashboard + +
+ + {% if parse_error %} +
{{ parse_error }}
+ {% endif %} + +
+

{{ report_kicker }}

+

{{ report_title }}

+

{{ report_subtitle }}

+
+ +

Incident overview

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

CVSS score (0.0–10.0)

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

Description & impact

+
+

What happened

+

{{ what_happened|linebreaksbr }}

+ +

Impact

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

+ {% endif %} + +

Consequences if following up

+

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

+ +

Consequences if not following up

+

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

+
+ +

Response & resolution

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

{{ disclaimer }}

+ +
+
Sign:
+
Date:
+
+
+ + + + + diff --git a/front-end/styles/incident_report.css b/front-end/styles/incident_report.css new file mode 100644 index 0000000..29edcb1 --- /dev/null +++ b/front-end/styles/incident_report.css @@ -0,0 +1,204 @@ +/* Incident report page — grey + seafoam, minimal */ + +:root { + --bg: #f4f7f6; + --surface: #ffffff; + --text: #334155; + --muted: #64748b; + --seafoam: #5eead4; + --seafoam-dark: #0d9488; + --border: #cbd5e1; + --grey-bar: #e2e8f0; +} + +* { + box-sizing: border-box; +} + +body.incident-report-body { + margin: 0; + min-height: 100vh; + font-family: "Segoe UI", system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} + +.ir-wrap { + max-width: 900px; + margin: 0 auto; + padding: 2.5rem 1.5rem 4rem; +} + +.ir-actions { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.ir-back { + display: inline-block; + font-size: 0.9rem; + color: var(--seafoam-dark); + text-decoration: none; +} + +.ir-back:hover { + text-decoration: underline; +} + +.ir-btn-download { + font-size: 0.9rem; + font-family: inherit; + padding: 0.5rem 1.1rem; + border-radius: 999px; + border: 1px solid var(--seafoam-dark); + background: var(--surface); + color: var(--seafoam-dark); + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.ir-btn-download:hover { + background: rgba(94, 234, 212, 0.25); + color: #0f766e; +} + +.ir-parse-error { + background: #fef3c7; + border: 1px solid #fcd34d; + color: #92400e; + padding: 0.75rem 1rem; + border-radius: 8px; + margin-bottom: 1.5rem; + font-size: 0.9rem; +} + +.ir-header { + border-left: 4px solid var(--seafoam); + padding-left: 1.25rem; + margin-bottom: 2rem; +} + +.ir-kicker { + font-size: 0.75rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); + margin: 0 0 0.35rem; +} + +.ir-title { + font-size: 1.85rem; + font-weight: 600; + margin: 0 0 0.25rem; + color: #1e293b; +} + +.ir-subtitle { + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--seafoam-dark); + margin: 0; +} + +.ir-section-title { + font-size: 1rem; + font-weight: 600; + color: #1e293b; + margin: 2rem 0 0.75rem; + padding-bottom: 0.35rem; + border-bottom: 2px solid var(--seafoam); +} + +.ir-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.25rem 1.5rem; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); +} + +.ir-table { + width: 100%; + border-collapse: collapse; + font-size: 0.92rem; +} + +.ir-table th, +.ir-table td { + padding: 0.65rem 0.85rem; + text-align: left; + border-bottom: 1px solid var(--grey-bar); + vertical-align: top; +} + +.ir-table th { + width: 32%; + font-weight: 600; + color: #475569; + background: #f8fafc; +} + +.ir-table tr:last-child th, +.ir-table tr:last-child td { + border-bottom: none; +} + +.ir-cvss-table th { + width: 25%; + text-align: center; + background: #f1f5f9; +} + +.ir-cvss-table td { + text-align: center; + font-variant-numeric: tabular-nums; +} + +.ir-block h4 { + margin: 1rem 0 0.5rem; + font-size: 0.95rem; + color: #0f766e; +} + +.ir-block p { + margin: 0 0 0.75rem; + white-space: pre-wrap; +} + +.ir-block ul { + margin: 0; + padding-left: 1.25rem; +} + +.ir-block li { + margin-bottom: 0.35rem; +} + +.ir-disclaimer { + margin-top: 2.5rem; + padding: 1rem 1.25rem; + background: #f1f5f9; + border-radius: 8px; + font-size: 0.8rem; + color: var(--muted); + line-height: 1.55; +} + +.ir-sign { + margin-top: 2rem; + display: grid; + gap: 1rem; + font-size: 0.95rem; +} + +.ir-sign-row { + border-bottom: 1px solid var(--border); + padding-bottom: 0.35rem; + min-height: 2rem; +} From 33ce3193ef4075a22f957acb55cf636b0aa29b4e Mon Sep 17 00:00:00 2001 From: kaye-s Date: Fri, 10 Apr 2026 12:34:22 -0500 Subject: [PATCH 34/41] fixed merging errors and updated everything with the changes --- api/views.py | 18 +++++++++----- front-end/login.html | 13 +++------- front-end/register.html | 43 ++++++++++++++++++++++++---------- front-end/scripts/login.js | 7 +++++- front-end/vulnerabilities.html | 29 ++++++++++++++++++++--- 5 files changed, 77 insertions(+), 33 deletions(-) diff --git a/api/views.py b/api/views.py index 5f2803b..6b6dfd4 100644 --- a/api/views.py +++ b/api/views.py @@ -8,10 +8,15 @@ from django.http import JsonResponse import json from .services.ai_service import ask_ai -from django.contrib.auth.decorators import login_required -from django.contrib.auth import authenticate, login, logout from .models import CodeSubmission, File, Threat, CWE, User from django.db.models import Q +from django.shortcuts import render, redirect +from django.contrib.auth.hashers import check_password, make_password + +def require_login(request): + if "user_id" not in request.session: + return redirect("login") + return None def ask_ai_view(request): if request.method == "POST": @@ -113,15 +118,16 @@ def logout_view(request): # Dashboard # ------------------- def dashboard_view(request): - if "user_id" not in request.session: - return redirect("/login/") + if require_login(request): + return require_login(request) return render(request, 'index.html') # ------------------- # Dummy Code Submission # ------------------- -@login_required def submit_code(request): + if require_login(request): + return require_login(request) print("HIT SUBMIT VIEW") result = None @@ -182,7 +188,7 @@ def register_view(request): hashed_pass = make_password(password) user = User.objects.create(email=email, password_hash=hashed_pass) - request.session["user_id"] = user.user_id + request.session["user_email"] = user.email return redirect('dashboard') return render(request, 'register.html', {'error': error}) diff --git a/front-end/login.html b/front-end/login.html index 68261df..886a640 100644 --- a/front-end/login.html +++ b/front-end/login.html @@ -19,16 +19,9 @@

AutoPen Login

{% csrf_token %} - - - - - -
- -

- Don't have an account? -

+
+ +
diff --git a/front-end/register.html b/front-end/register.html index 69ccf91..21bb538 100644 --- a/front-end/register.html +++ b/front-end/register.html @@ -1,27 +1,44 @@ - +{% load static %} + - Register + Register - AutoPen + + + -

Create Account

+ -
-Back to Login + \ No newline at end of file diff --git a/front-end/scripts/login.js b/front-end/scripts/login.js index 2b017cd..4657bb0 100644 --- a/front-end/scripts/login.js +++ b/front-end/scripts/login.js @@ -17,7 +17,12 @@ const form = document.querySelector("form"); const button = document.querySelector(".login-btn"); form.addEventListener("submit", () => { - button.innerText = "Logging in..."; + if (button.innerText.toLowerCase().includes("register")) { + button.innerText = "Creating account..."; + } else { + button.innerText = "Logging in..."; + } + button.classList.add("loading"); button.disabled = true; }); diff --git a/front-end/vulnerabilities.html b/front-end/vulnerabilities.html index c3a5e68..9cec22b 100644 --- a/front-end/vulnerabilities.html +++ b/front-end/vulnerabilities.html @@ -4,17 +4,40 @@ Vulnerabilities + - + +
+ + + +
+ From 52e22dec5768dda979f4e3ee49bbff2f633a6e10 Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Sat, 11 Apr 2026 18:34:07 -0500 Subject: [PATCH 35/41] upload file and connect to backend to generate report --- api/services/incident_report_ai.py | 2 +- api/views.py | 189 +++++++++++++++++++---------- front-end/index.html | 4 +- 3 files changed, 129 insertions(+), 66 deletions(-) diff --git a/api/services/incident_report_ai.py b/api/services/incident_report_ai.py index 8eafb6e..58d8a93 100644 --- a/api/services/incident_report_ai.py +++ b/api/services/incident_report_ai.py @@ -52,7 +52,7 @@ def generate_incident_report_ai_payload(passage: dict[str, Any]) -> str: """ user_prompt = build_incident_report_user_prompt(passage) kwargs: dict[str, Any] = { - "model": "gpt-4.1-mini", + "model": "gpt-5.4-nano", "messages": [ {"role": "system", "content": _SYSTEM}, {"role": "user", "content": user_prompt}, diff --git a/api/views.py b/api/views.py index c7fd554..d684f4d 100644 --- a/api/views.py +++ b/api/views.py @@ -17,10 +17,108 @@ from django.contrib.auth import authenticate, login, logout from .models import CodeSubmission, File, Threat from pathlib import Path +import re import tempfile import shutil from .utils.prescan import run_semgrep, run_gitleaks +# Max upload size for scan (bytes). +MAX_UPLOAD_BYTES = 2 * 1024 * 1024 + + +def _safe_upload_basename(original_name: str) -> str: + """Return a single path segment safe for writing under a temp directory.""" + base = Path(original_name).name + if not base or base in {".", ".."}: + return "upload.txt" + safe = re.sub(r"[^a-zA-Z0-9._-]", "_", base) + if len(safe) > 200: + stem = Path(safe).stem[:150] + suffix = Path(safe).suffix[:20] + safe = stem + suffix + return safe or "upload.txt" + + +def _read_uploaded_text(uploaded) -> str: + """Read uploaded file as text with a hard size cap.""" + chunk = uploaded.read(MAX_UPLOAD_BYTES + 1) + if len(chunk) > MAX_UPLOAD_BYTES: + raise ValueError( + f"File is too large (max {MAX_UPLOAD_BYTES // (1024 * 1024)} MiB)." + ) + return chunk.decode("utf-8", errors="replace") + + +def _run_incident_scan(request, code: str, source: dict): + """ + Shared pipeline: write code to a temp file, pre-scan, call OpenAI JSON report, render HTML. + source: {"origin": "upload"|"paste", "filename": str} + """ + tmp_dir_path = Path(tempfile.mkdtemp(prefix="autopen_")) + try: + if source.get("origin") == "upload": + fname = _safe_upload_basename(source.get("filename") or "upload.txt") + else: + fname = "pasted_code.py" + target_file_path = tmp_dir_path / fname + target_file_path.write_text(code, encoding="utf-8") + + # Pre-process: run semgrep + gitleaks against the temporary file. + semgrep_report = run_semgrep(str(target_file_path)) + gitleaks_report = run_gitleaks(str(target_file_path)) + + # Keep the prompt size bounded. + max_code_chars = 8000 + truncated = code[:max_code_chars] + trunc_note = "" + if len(code) > max_code_chars: + trunc_note = f"\n\n[NOTE] Code was truncated to the first {max_code_chars} characters." + + # Keep tool findings compact to reduce token usage. + semgrep_results = semgrep_report.get("results", []) or [] + gitleaks_results = gitleaks_report.get("results", []) or [] + semgrep_results = semgrep_results[:20] + gitleaks_results = gitleaks_results[:20] + + # Assemble a single "passage" for OpenAI analysis. + passage = { + "source": source, + "user_code": truncated + trunc_note, + "semgrep": { + "error": semgrep_report.get("error"), + "results": semgrep_results, + }, + "gitleaks": { + "error": gitleaks_report.get("error"), + "results": gitleaks_results, + }, + } + + raw_json = generate_incident_report_ai_payload(passage) + ai_data = parse_llm_json(raw_json) + parse_error = None + if not ai_data: + parse_error = ( + "The model returned empty or non-JSON output; placeholder values are shown where needed." + ) + + ctx = merge_incident_report_context( + request=request, + ai=ai_data, + parse_error=parse_error, + ) + ctx["disclaimer"] = DISCLAIMER_TEXT + return render(request, "incident_report.html", ctx) + except Exception as e: + return render( + request, + "index.html", + {"result": f"Analysis failed: {type(e).__name__}: {e}"}, + ) + finally: + # Best-effort cleanup of temporary files. + shutil.rmtree(tmp_dir_path, ignore_errors=True) + def ask_ai_view(request): if request.method == "POST": data = json.loads(request.body) @@ -123,71 +221,36 @@ def submit_code(request): if request.method != "POST": return redirect("dashboard") - code = request.POST.get("code", "").strip() - if not code: - return render( - request, - "index.html", - {"result": "No code submitted."}, - ) - - # Create an isolated temp directory so semgrep/gitleaks can scan it safely. - tmp_dir_path = Path(tempfile.mkdtemp(prefix="autopen_")) - try: - target_file_path = tmp_dir_path / "target.py" - target_file_path.write_text(code, encoding="utf-8") - - # Pre-process: run semgrep + gitleaks against the temporary file. - semgrep_report = run_semgrep(str(target_file_path)) - gitleaks_report = run_gitleaks(str(target_file_path)) - - # Keep the prompt size bounded. - max_code_chars = 8000 - truncated = code[:max_code_chars] - trunc_note = "" - if len(code) > max_code_chars: - trunc_note = f"\n\n[NOTE] Code was truncated to the first {max_code_chars} characters." - - # Keep tool findings compact to reduce token usage. - semgrep_results = semgrep_report.get("results", []) or [] - gitleaks_results = gitleaks_report.get("results", []) or [] - semgrep_results = semgrep_results[:20] - gitleaks_results = gitleaks_results[:20] - - # Assemble a single "passage" for OpenAI analysis. - passage = { - "user_code": truncated + trunc_note, - "semgrep": { - "error": semgrep_report.get("error"), - "results": semgrep_results, - }, - "gitleaks": { - "error": gitleaks_report.get("error"), - "results": gitleaks_results, - }, - } + uploaded = request.FILES.get("file") + code_paste = request.POST.get("code", "").strip() - raw_json = generate_incident_report_ai_payload(passage) - ai_data = parse_llm_json(raw_json) - parse_error = None - if not ai_data: - parse_error = ( - "The model returned empty or non-JSON output; placeholder values are shown where needed." + # 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) - ctx = merge_incident_report_context( - request=request, - ai=ai_data, - parse_error=parse_error, - ) - ctx["disclaimer"] = DISCLAIMER_TEXT - return render(request, "incident_report.html", ctx) - except Exception as e: - return render( + if code_paste: + return _run_incident_scan( request, - "index.html", - {"result": f"Analysis failed: {type(e).__name__}: {e}"}, + code_paste, + {"origin": "paste", "filename": "pasted_code.py"}, ) - finally: - # Best-effort cleanup of temporary files. - shutil.rmtree(tmp_dir_path, ignore_errors=True) + + return render( + request, + "index.html", + {"result": "No code submitted. Upload a file or paste code."}, + ) diff --git a/front-end/index.html b/front-end/index.html index 3386def..c0e77fb 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -71,7 +71,7 @@

Low Vulnerabilities

Upload Code for Analysis

-
+ {% csrf_token %}
@@ -82,7 +82,7 @@

Upload Code for Analysis

From 80555a36353ec8256b73df4b030e6af496e88e9d Mon Sep 17 00:00:00 2001 From: JacobLind1 Date: Tue, 21 Apr 2026 12:57:42 -0500 Subject: [PATCH 36/41] Added backend functionality/connection for dashboard recent scans --- api/models.py | 6 ++ api/urls.py | 5 ++ api/views.py | 159 ++++++++++++++++++++++++++++++++++--- front-end/index.html | 22 ++--- front-end/login.html | 47 +++++------ front-end/register.html | 43 +++++++--- front-end/scripts/login.js | 38 +++++++++ front-end/scripts/main.js | 18 +++++ front-end/styles/login.css | 148 ++++++++++++++++++++++++++++++++++ 9 files changed, 428 insertions(+), 58 deletions(-) create mode 100644 front-end/scripts/login.js create mode 100644 front-end/scripts/main.js create mode 100644 front-end/styles/login.css diff --git a/api/models.py b/api/models.py index b66898d..b9a5f31 100644 --- a/api/models.py +++ b/api/models.py @@ -20,10 +20,16 @@ class CodeSubmission(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions') submission_name = models.CharField(max_length=255, null=True, blank=True) uploaded_at = models.DateTimeField(auto_now_add=True) + overall_risk_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) simplified_summary = models.TextField(null=True, blank=True) detailed_summary = models.TextField(null=True, blank=True) + scan_status = models.CharField(max_length=50, null=True, blank=True) + risk_level = models.CharField(max_length=20, null=True, blank=True) + incident_id = models.CharField(max_length=100, null=True, blank=True) + report_html_path = models.TextField(null=True, blank=True) + def __str__(self): return f"{self.submission_name} by {self.user.email}" diff --git a/api/urls.py b/api/urls.py index 90bd453..8d05162 100644 --- a/api/urls.py +++ b/api/urls.py @@ -7,6 +7,9 @@ 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'), @@ -14,4 +17,6 @@ # Check status / get results of a submission path('submission//', SubmissionStatusView.as_view(), name='submission_status'), + + path('vulnerabilities/', views.vulnerability_list, name='vulnerability_list'), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index d684f4d..c9f0741 100644 --- a/api/views.py +++ b/api/views.py @@ -15,16 +15,22 @@ ) from django.contrib.auth.decorators import login_required from django.contrib.auth import authenticate, login, logout -from .models import CodeSubmission, File, Threat +from .models import CodeSubmission, File, Threat, CWE, User from pathlib import Path import re import tempfile import shutil from .utils.prescan import run_semgrep, run_gitleaks +from django.shortcuts import render, redirect +from django.contrib.auth.hashers import check_password, make_password # Max upload size for scan (bytes). MAX_UPLOAD_BYTES = 2 * 1024 * 1024 +def require_login(request): + if "user_id" not in request.session: + return redirect("login") + return None def _safe_upload_basename(original_name: str) -> str: """Return a single path segment safe for writing under a temp directory.""" @@ -55,14 +61,33 @@ def _run_incident_scan(request, code: str, source: dict): source: {"origin": "upload"|"paste", "filename": str} """ tmp_dir_path = Path(tempfile.mkdtemp(prefix="autopen_")) + try: if source.get("origin") == "upload": fname = _safe_upload_basename(source.get("filename") or "upload.txt") else: fname = "pasted_code.py" + target_file_path = tmp_dir_path / fname target_file_path.write_text(code, encoding="utf-8") + # Create submission record early + user_id = request.session.get("user_id") + if not user_id: + return render( + request, + "index.html", + {"result": "You must be logged in to run a scan."}, + ) + + user = User.objects.get(user_id=user_id) + + submission = CodeSubmission.objects.create( + user=user, + submission_name=fname, + scan_status="Running", + ) + # Pre-process: run semgrep + gitleaks against the temporary file. semgrep_report = run_semgrep(str(target_file_path)) gitleaks_report = run_gitleaks(str(target_file_path)) @@ -96,11 +121,50 @@ def _run_incident_scan(request, code: str, source: dict): raw_json = generate_incident_report_ai_payload(passage) ai_data = parse_llm_json(raw_json) + parse_error = None if not ai_data: parse_error = ( "The model returned empty or non-JSON output; placeholder values are shown where needed." ) + ai_data = {} + + # Build simple incident ID + incident_id = f"CYB-{submission.uploaded_at.year}-{submission.submission_id}" + + # If you later save a real HTML file, replace this with the actual path + report_path = f"reports/{incident_id}.html" + + # Safely convert score + base_score_raw = ( + ai_data.get("cvss", {}).get("base") + if isinstance(ai_data.get("cvss"), dict) + else None + ) + + base_score = None + try: + if base_score_raw not in (None, "", "N/A"): + base_score = Decimal(str(base_score_raw)) + except Exception: + base_score = None + + # Update dashboard/report fields + submission.submission_name = fname + submission.risk_level = ai_data.get("severity_level", "Informational") + submission.scan_status = "Completed" + submission.overall_risk_score = base_score + submission.simplified_summary = ai_data.get( + "status", + "No findings evidenced" + ) + submission.detailed_summary = ai_data.get( + "what_happened", + "No additional analysis details available." + ) + submission.incident_id = incident_id + submission.report_html_path = report_path + submission.save() ctx = merge_incident_report_context( request=request, @@ -108,7 +172,11 @@ def _run_incident_scan(request, code: str, source: dict): parse_error=parse_error, ) ctx["disclaimer"] = DISCLAIMER_TEXT + ctx["submission_id"] = submission.submission_id + ctx["incident_id"] = incident_id + return render(request, "incident_report.html", ctx) + except Exception as e: return render( request, @@ -192,31 +260,48 @@ def get(self, request, submission_id): def login_view(request): error = None if request.method == "POST": - username = request.POST['username'] - password = request.POST['password'] - user = authenticate(request, username=username, password=password) - if user: - login(request, user) - return redirect('dashboard') + email = request.POST.get("email") + password = request.POST.get("password") + + if not email or not password: + error = "Enter Email and Password" else: - error = "Invalid username or password" + try: + user = User.objects.get(email=email) + + if check_password(password, user.password_hash): + request.session["user_id"] = user.user_id + request.session["user_email"] = user.email + return redirect("dashboard") + else: + error = "Invalid Email or Password" + except User.DoesNotExist: + error = "Invalid Email or Password" return render(request, 'login.html', {'error': error}) def logout_view(request): - logout(request) + request.session.flush() return redirect('login') + # ------------------- # Dashboard # ------------------- -@login_required(login_url='/login/') # Redirects to login if not logged in +#@login_required(login_url='/login/') # Redirects to login if not logged in def dashboard_view(request): - return render(request, 'index.html') + if "user_id" not in request.session: + return redirect("/login/") + + scans = CodeSubmission.objects.filter( + user_id=request.session["user_id"] + ).order_by("-uploaded_at")[:10] + + return render(request, "index.html", {"scans": scans}) # ------------------- # Dummy Code Submission # ------------------- -@login_required +#@login_required def submit_code(request): if request.method != "POST": return redirect("dashboard") @@ -254,3 +339,53 @@ def submit_code(request): "index.html", {"result": "No code submitted. Upload a file or paste code."}, ) + +def vulnerability_list(request): + query = request.GET.get('q', '').strip() + severity = request.GET.get('severity', '').strip() + + vulnerabilities = CWE.objects.all() + + # ONLY apply search if actually typed + if query: + vulnerabilities = vulnerabilities.filter( + Q(name__icontains=query) | + Q(description__icontains=query) | + Q(cwe_id__icontains=query) + ) + + # Apply severity filter independently + if severity: + vulnerabilities = vulnerabilities.filter(severity=severity) + + context = { + 'vulnerabilities': vulnerabilities, + 'query': query, + 'selected_severity': severity, + } + + return render(request, 'vulnerabilities.html', context) +# ----------------- +# User Registration +# ----------------- +def register_view(request): + error = None + + if request.method == "POST": + email = request.POST.get("email") + password = request.POST.get("password") + + if not email or not password: + error = "Enter Email and Password" + + elif User.objects.filter(email=email).exists(): + error = "Email Already in Use" + + else: + hashed_pass = make_password(password) + + user = User.objects.create(email=email, password_hash=hashed_pass) + request.session["user_email"] = user.email + return redirect('dashboard') + + return render(request, 'register.html', {'error': error}) diff --git a/front-end/index.html b/front-end/index.html index c0e77fb..327f39d 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -31,7 +31,7 @@

AutoPen

Penetration Testing Dashboard

-

User: {{ request.user }}

+

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

System Status: Active
{% csrf_token %} @@ -51,7 +51,7 @@

Penetration Testing Dashboard

Total Scans

-

128

+

{{scans|length}}

Critical Vulnerabilities

@@ -115,18 +115,20 @@

Recent Scan Results

+ {% for scan in scans %} - example.com - 02/14/2026 - Critical - Completed + {{ scan.submission_name|default:"Unnamed submission" }} + {{ scan.uploaded_at|date:"m/d/Y" }} + + {{ scan.risk_level|default:"N/A" }} + + {{ scan.scan_status|default:"Unknown" }} + {% empty %} - test-server.net - 02/12/2026 - Low - Completed + No scans yet. + {% endfor %}
diff --git a/front-end/login.html b/front-end/login.html index 06080e9..886a640 100644 --- a/front-end/login.html +++ b/front-end/login.html @@ -1,42 +1,43 @@ {% load static %} - + Login - AutoPen - + - + -
-
+ +
+ +
+ +
+ +
+ 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/main.js b/front-end/scripts/main.js index 1385644..ef04ed0 100644 --- a/front-end/scripts/main.js +++ b/front-end/scripts/main.js @@ -4,15 +4,49 @@ window.addEventListener("load", () => { }); 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"); - main.classList.toggle("shift"); + if (main) { + main.classList.toggle("shift"); + } + syncAria(); }); -}); \ No newline at end of file + 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/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 index 29edcb1..a2aa1c2 100644 --- a/front-end/styles/incident_report.css +++ b/front-end/styles/incident_report.css @@ -24,6 +24,14 @@ body.incident-report-body { 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; @@ -77,6 +85,12 @@ body.incident-report-body { 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; @@ -202,3 +216,11 @@ body.incident-report-body { 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 index 97025e1..cff6018 100644 --- a/front-end/styles/login.css +++ b/front-end/styles/login.css @@ -79,6 +79,15 @@ body.loaded { 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; @@ -119,6 +128,12 @@ body.loaded { margin-bottom: 10px; } +.success { + color: #86efac; + text-align: center; + margin-bottom: 10px; +} + /* ================= REGISTER LINK ================= */ .register { text-align: center; diff --git a/front-end/styles/style.css b/front-end/styles/style.css index 08a5ded..142191a 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -44,23 +44,55 @@ body.loaded { left: 0; } -.sidebar h2 { - letter-spacing: 1px; - font-weight: 600; -} -.sidebar ul { +.sidebar-nav ul { list-style: none; } -.sidebar ul li { +.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 ul li:hover { +.sidebar-link:hover { color: #c084fc; - text-shadow: 0 0 8px rgba(168, 85, 247, 0.6); + 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 ================= */ @@ -382,3 +414,514 @@ input, select { 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 index 9cec22b..116f78f 100644 --- a/front-end/vulnerabilities.html +++ b/front-end/vulnerabilities.html @@ -1,44 +1,18 @@ {% load static %} - + - Vulnerabilities + + + Vulnerabilities — AutoPen - + + - + - -
- - - -
- - +{% include "includes/app_sidebar.html" %}
@@ -120,5 +94,6 @@

All Vulnerabilities

+{% include "includes/ai_assistant.html" %} - \ No newline at end of file + From d429083dfcfeaf5a9f63d046344271fb784f7d24 Mon Sep 17 00:00:00 2001 From: zhangtingen Date: Sun, 3 May 2026 21:57:25 -0500 Subject: [PATCH 41/41] Update:personal information --- api/migrations/0005_user_llm_token_totals.py | 23 ++++++ api/models.py | 2 + api/services/ai_service.py | 4 +- api/services/incident_report_ai.py | 10 ++- api/tests.py | 18 ++++- api/urls.py | 1 + api/utils/incident_report.py | 25 ++++++- api/utils/openai_usage.py | 23 ++++++ api/views.py | 74 ++++++++++++++++++- front-end/includes/app_sidebar.html | 2 +- front-end/personal_info.html | 77 ++++++++++++++++++++ front-end/styles/style.css | 72 ++++++++++++++++++ 12 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 api/migrations/0005_user_llm_token_totals.py create mode 100644 api/utils/openai_usage.py create mode 100644 front-end/personal_info.html 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/models.py b/api/models.py index e3ba9b8..b354ea4 100644 --- a/api/models.py +++ b/api/models.py @@ -39,6 +39,8 @@ class User(models.Model): 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 diff --git a/api/services/ai_service.py b/api/services/ai_service.py index fad18ea..f38c435 100644 --- a/api/services/ai_service.py +++ b/api/services/ai_service.py @@ -10,4 +10,6 @@ def ask_ai(user_text, model=None): messages=[{"role": "user", "content": str(user_text)}], ) - return resp.choices[0].message.content \ No newline at end of file + 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/incident_report_ai.py b/api/services/incident_report_ai.py index 9760f8f..90d4632 100644 --- a/api/services/incident_report_ai.py +++ b/api/services/incident_report_ai.py @@ -46,9 +46,11 @@ def build_incident_report_user_prompt(passage: dict[str, Any]) -> str: return schema.strip() + "\n" + passage_json -def generate_incident_report_ai_payload(passage: dict[str, Any], model: str | None = None) -> str: +def generate_incident_report_ai_payload( + passage: dict[str, Any], model: str | None = None +) -> tuple[str, Any]: """ - Calls the chat completion API and returns raw message content (expected JSON object). + Calls the chat completion API and returns (message content, usage or None). """ user_prompt = build_incident_report_user_prompt(passage) kwargs: dict[str, Any] = { @@ -66,4 +68,6 @@ def generate_incident_report_ai_payload(passage: dict[str, Any], model: str | No kwargs.pop("response_format", None) resp = client.chat.completions.create(**kwargs) - return (resp.choices[0].message.content or "").strip() + content = (resp.choices[0].message.content or "").strip() + usage = getattr(resp, "usage", None) + return content, usage diff --git a/api/tests.py b/api/tests.py index d248458..535b359 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,14 +1,30 @@ -from django.test import TestCase +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): diff --git a/api/urls.py b/api/urls.py index 5efc2e7..3bfbaba 100644 --- a/api/urls.py +++ b/api/urls.py @@ -18,6 +18,7 @@ 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'), diff --git a/api/utils/incident_report.py b/api/utils/incident_report.py index de378bb..df21a54 100644 --- a/api/utils/incident_report.py +++ b/api/utils/incident_report.py @@ -7,9 +7,25 @@ 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]: """ @@ -97,7 +113,14 @@ def merge_incident_report_context( if user is not None and getattr(user, "is_authenticated", False): reported_by = user.get_username() or _as_str(getattr(user, "email", ""), "Unknown") else: - reported_by = "Unknown" + # 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")) 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/views.py b/api/views.py index b88b770..b984e27 100644 --- a/api/views.py +++ b/api/views.py @@ -22,9 +22,11 @@ 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 @@ -226,7 +228,10 @@ def _run_incident_scan( }, } - raw_json = generate_incident_report_ai_payload(passage, model=user_settings.ai_model) + 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 @@ -304,10 +309,11 @@ def assistant_chat_view(request): 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 = ask_ai(user_text, model=model) + 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}) + return JsonResponse({"reply": reply or ""}) def scan_view(request): target_path = "/path/to/code" # you could get this from request.POST @@ -679,6 +685,60 @@ def settings_view(request): "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 # ----------------- @@ -744,7 +804,7 @@ def report_detail_view(request, submission_id): return redirect("/login/") submission = get_object_or_404( - CodeSubmission, + CodeSubmission.objects.select_related("user"), submission_id=submission_id, user__department=current_user.department ) @@ -757,6 +817,12 @@ def report_detail_view(request, submission_id): 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 diff --git a/front-end/includes/app_sidebar.html b/front-end/includes/app_sidebar.html index 22a9ec0..b10db2c 100644 --- a/front-end/includes/app_sidebar.html +++ b/front-end/includes/app_sidebar.html @@ -38,7 +38,7 @@ +
+{% include "includes/ai_assistant.html" %} + + diff --git a/front-end/styles/style.css b/front-end/styles/style.css index 142191a..e49342a 100644 --- a/front-end/styles/style.css +++ b/front-end/styles/style.css @@ -112,6 +112,78 @@ body.loaded { 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;