larxius commited on
Commit
1c92e29
·
verified ·
1 Parent(s): f31b9ac

Update backend/models.py

Browse files
Files changed (1) hide show
  1. backend/models.py +415 -0
backend/models.py ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.insert(0, os.path.abspath('backend'))
4
+
5
+ from bs4 import BeautifulSoup
6
+ from celery import Celery
7
+ from celery.schedules import crontab
8
+ from collections import defaultdict
9
+ from scanners.base_scanner import (
10
+ active_scan_logs, add_log, get_scan_logs, parse_domain,
11
+ cleanup_scan_logs, schedule_log_cleanup, emit_scan_progress
12
+ )
13
+ from scanners import get_pipeline, get_phases, build_scanner, apply_scan_options
14
+ try:
15
+ from backend.utils.fuzzer_engine import ContextAwareFuzzer
16
+ except ImportError:
17
+ from utils.fuzzer_engine import ContextAwareFuzzer
18
+ from cryptography import x509
19
+ from cryptography.hazmat.backends import default_backend
20
+ from datetime import datetime, timezone
21
+ from datetime import datetime, timezone, timedelta
22
+ from datetime import datetime, timezone, timezone
23
+ from dotenv import load_dotenv
24
+ load_dotenv()
25
+
26
+ import stripe
27
+ from flask import Blueprint, request, jsonify, current_app, send_from_directory
28
+ from werkzeug.utils import secure_filename
29
+ from flask import Blueprint, send_file, jsonify, request
30
+ from flask import Flask
31
+ from flask import jsonify
32
+ from flask import render_template
33
+ from flask import request, abort, g, Response, make_response
34
+ from flask_cors import CORS
35
+ from flask_limiter import Limiter
36
+ from flask_limiter.util import get_remote_address
37
+ from flask_socketio import SocketIO, emit, join_room, leave_room
38
+ from flask_sqlalchemy import SQLAlchemy
39
+ from functools import wraps
40
+ from markupsafe import escape # always available with Flask
41
+ from reportlab.lib import colors
42
+ from reportlab.lib.pagesizes import letter
43
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
44
+ from reportlab.pdfgen import canvas
45
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Image, Flowable
46
+ from reportlab.graphics.shapes import Drawing
47
+ from reportlab.graphics.charts.barcharts import VerticalBarChart
48
+ from sqlalchemy import event
49
+ from sqlalchemy import func
50
+ from sqlalchemy import inspect, text
51
+ from sqlalchemy import text
52
+ from sqlalchemy.engine import Engine
53
+ from typing import Any
54
+ from typing import Any, Callable
55
+ from typing import Callable
56
+ from typing import Literal
57
+ from urllib.parse import urljoin, urlparse
58
+ from urllib.parse import urlparse
59
+ import base64
60
+ import bcrypt
61
+ import concurrent.futures
62
+ from backend.utils.email_service import (
63
+ send_welcome_email,
64
+ send_scan_started,
65
+ send_scan_completed,
66
+ send_scan_failed,
67
+ send_critical_alert
68
+ )
69
+
70
+ import hashlib
71
+ import html
72
+ import io
73
+ import itertools
74
+ import json
75
+ import jwt
76
+ import math
77
+ import os
78
+ import re
79
+ import re, time, ipaddress, os, hashlib, threading
80
+ import requests
81
+ import socket
82
+ import sqlite3
83
+ import ssl
84
+ import statistics
85
+ import threading
86
+ import time
87
+ import traceback
88
+ import urllib.error
89
+ import urllib.parse
90
+ import urllib.request
91
+ import urllib3
92
+ import uuid
93
+ import ipaddress
94
+
95
+
96
+
97
+ from .extensions import db, celery, socketio, limiter
98
+
99
+
100
+ # --- From models.py ---
101
+
102
+ class Organization(db.Model):
103
+ __tablename__ = 'organizations'
104
+ def __init__(self, **kwargs): super().__init__(**kwargs)
105
+
106
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
107
+ name = db.Column(db.String(150), nullable=False)
108
+ subscription_tier = db.Column(db.String(50), nullable=False, default='free')
109
+ status = db.Column(db.String(50), default='active')
110
+ api_key = db.Column(db.String(100), unique=True, nullable=True)
111
+ webhook_url = db.Column(db.String(500), nullable=True)
112
+ report_logo_url = db.Column(db.String(500), nullable=True)
113
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
114
+
115
+ users = db.relationship('User', backref='organization', lazy=True)
116
+ scans = db.relationship('Scan', backref='organization', lazy=True, cascade="all, delete-orphan")
117
+
118
+
119
+ class Role(db.Model):
120
+ __tablename__ = 'roles'
121
+ def __init__(self, **kwargs): super().__init__(**kwargs)
122
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
123
+ name = db.Column(db.String(50), unique=True, nullable=False)
124
+
125
+ class User(db.Model):
126
+ __tablename__ = 'users'
127
+ def __init__(self, **kwargs): super().__init__(**kwargs)
128
+
129
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
130
+ email = db.Column(db.String(120), unique=True, nullable=False)
131
+ password_hash = db.Column(db.String(128), nullable=False)
132
+ first_name = db.Column(db.String(100), nullable=True)
133
+ last_name = db.Column(db.String(100), nullable=True)
134
+ role = db.Column(db.String(50), nullable=False, default='org_admin') # super_admin, support_engineer, org_admin, soc_analyst, executive, read_only
135
+ org_id = db.Column(db.String(36), db.ForeignKey('organizations.id'), nullable=True)
136
+
137
+ failed_login_attempts = db.Column(db.Integer, default=0)
138
+ locked_until = db.Column(db.DateTime, nullable=True)
139
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
140
+
141
+ # Policy Consent & Security Fields
142
+ terms_accepted_at = db.Column(db.DateTime, nullable=True)
143
+ privacy_policy_agreed_at = db.Column(db.DateTime, nullable=True)
144
+ policy_version_agreed = db.Column(db.String(20), default='v1.0')
145
+ mfa_enabled = db.Column(db.Boolean, default=False)
146
+ mfa_secret = db.Column(db.String(100), nullable=True)
147
+ reset_token = db.Column(db.String(255), nullable=True)
148
+ reset_token_expires = db.Column(db.DateTime, nullable=True)
149
+ email_verified = db.Column(db.Boolean, default=False)
150
+
151
+ scans = db.relationship('Scan', backref='user', lazy=True, cascade="all, delete-orphan")
152
+ alert_settings = db.relationship('AlertSettings', backref='user', uselist=False, lazy=True, cascade="all, delete-orphan")
153
+
154
+ @property
155
+ def subscription_tier(self):
156
+ if self.organization:
157
+ return self.organization.subscription_tier
158
+ return 'free'
159
+
160
+ @subscription_tier.setter
161
+ def subscription_tier(self, value):
162
+ if self.organization:
163
+ self.organization.subscription_tier = value
164
+
165
+ def set_password(self, password):
166
+ salt = bcrypt.gensalt()
167
+ self.password_hash = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
168
+
169
+ def check_password(self, password):
170
+ if not self.password_hash or not password:
171
+ return False
172
+ try:
173
+ return bcrypt.checkpw(password.encode('utf-8'), self.password_hash.encode('utf-8'))
174
+ except (ValueError, TypeError, AttributeError):
175
+ return False
176
+
177
+ class SubscriptionTier(db.Model):
178
+ __tablename__ = 'subscription_tiers'
179
+ def __init__(self, **kwargs): super().__init__(**kwargs)
180
+ id = db.Column(db.String(50), primary_key=True)
181
+ name = db.Column(db.String(100), nullable=False)
182
+ monthly_price = db.Column(db.Integer, nullable=False, default=0)
183
+ yearly_price = db.Column(db.Integer, nullable=False, default=0)
184
+
185
+ class AuditLog(db.Model):
186
+ __tablename__ = 'audit_logs'
187
+ def __init__(self, **kwargs): super().__init__(**kwargs)
188
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
189
+ admin_id = db.Column(db.String(36), nullable=False)
190
+ action = db.Column(db.String(255), nullable=False)
191
+ target_id = db.Column(db.String(100), nullable=True)
192
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
193
+
194
+ class Payment(db.Model):
195
+ __tablename__ = 'payments'
196
+ def __init__(self, **kwargs): super().__init__(**kwargs)
197
+
198
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
199
+ org_id = db.Column(db.String(36), db.ForeignKey('organizations.id', ondelete='CASCADE'), nullable=True)
200
+ user_id = db.Column(db.String(36), db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
201
+ razorpay_payment_id = db.Column(db.String(100), unique=True, nullable=True)
202
+ razorpay_order_id = db.Column(db.String(100), nullable=True)
203
+ stripe_session_id = db.Column(db.String(100), unique=True, nullable=True)
204
+ stripe_payment_id = db.Column(db.String(100), unique=True, nullable=True)
205
+ tier_id = db.Column(db.String(50), nullable=False)
206
+ amount = db.Column(db.Integer, nullable=False)
207
+ currency = db.Column(db.String(10), default='USD')
208
+ status = db.Column(db.String(50), default='successful')
209
+ created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
210
+
211
+ class Scan(db.Model):
212
+ __tablename__ = 'scans'
213
+ def __init__(self, **kwargs): super().__init__(**kwargs)
214
+
215
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
216
+ org_id = db.Column(db.String(36), db.ForeignKey('organizations.id'), nullable=False)
217
+ user_id = db.Column(db.String(36), db.ForeignKey('users.id'), nullable=False)
218
+ target_url = db.Column(db.String(500), nullable=False)
219
+ scan_type = db.Column(db.String(50), nullable=False, default='Full') # Full, Port, SSL, OWASP
220
+ status = db.Column(db.String(50), nullable=False, default='queued') # queued, scanning, completed, failed
221
+ security_score = db.Column(db.Integer, nullable=True) # 0 to 100
222
+ started_at = db.Column(db.DateTime, default=datetime.utcnow)
223
+ completed_at = db.Column(db.DateTime, nullable=True)
224
+ auth_headers = db.Column(db.JSON, nullable=True) # For authenticated scanning
225
+ scan_options = db.Column(db.JSON, nullable=True) # crawl_depth, exclude_paths, enable_red_team
226
+ ssl_info = db.Column(db.JSON, nullable=True) # Cached SSL certificate info
227
+
228
+ duration_seconds = db.Column(db.Integer, nullable=True)
229
+ error_message = db.Column(db.Text, nullable=True)
230
+ critical_count = db.Column(db.Integer, default=0)
231
+ high_count = db.Column(db.Integer, default=0)
232
+ medium_count = db.Column(db.Integer, default=0)
233
+ low_count = db.Column(db.Integer, default=0)
234
+
235
+ vulnerabilities = db.relationship('Vulnerability', backref='scan', lazy=True, cascade="all, delete-orphan")
236
+
237
+ def __init__(self, org_id, user_id, target_url, scan_type='Full', status='queued', security_score=None, started_at=None, completed_at=None, auth_headers=None, scan_options=None, id=None):
238
+ if id: self.id = id
239
+ self.org_id = org_id
240
+ self.user_id = user_id
241
+ self.target_url = target_url
242
+ self.scan_type = scan_type
243
+ self.status = status
244
+ self.security_score = security_score
245
+ self.auth_headers = auth_headers
246
+ self.scan_options = scan_options
247
+ if started_at: self.started_at = started_at
248
+ if completed_at: self.completed_at = completed_at
249
+
250
+ class Vulnerability(db.Model):
251
+ __tablename__ = 'vulnerabilities'
252
+ def __init__(self, **kwargs): super().__init__(**kwargs)
253
+
254
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
255
+ scan_id = db.Column(db.String(36), db.ForeignKey('scans.id'), nullable=False)
256
+ title = db.Column(db.String(200), nullable=False)
257
+ severity = db.Column(db.String(50), nullable=False) # Critical, High, Medium, Low
258
+ category = db.Column(db.String(100), nullable=False) # SSL/TLS, Port, Security Headers, Injection
259
+ description = db.Column(db.Text, nullable=False)
260
+ remediation = db.Column(db.Text, nullable=False)
261
+ cvss_score = db.Column(db.Float, nullable=False)
262
+ detected_at = db.Column(db.DateTime, default=datetime.utcnow)
263
+ evidence = db.Column(db.Text, default="")
264
+ payload = db.Column(db.Text, default="")
265
+ request_details = db.Column(db.Text, default="")
266
+ response_details = db.Column(db.Text, default="")
267
+ is_false_positive = db.Column(db.Boolean, default=False)
268
+ cwe_ids = db.Column(db.JSON, nullable=True)
269
+ owasp_category = db.Column(db.String(100), nullable=True)
270
+ exploit_poc = db.Column(db.JSON, nullable=True)
271
+ remediation_code = db.Column(db.Text, nullable=True)
272
+
273
+ status = db.Column(db.String(50), nullable=False, default='open') # open, in_progress, resolved, ignored
274
+ remediated_at = db.Column(db.DateTime, nullable=True)
275
+ remediated_by = db.Column(db.String(36), nullable=True)
276
+
277
+ def __init__(self, scan_id, title, severity, category, description, remediation, cvss_score, detected_at=None, id=None, evidence="", payload="", request_details="", response_details="", cwe_ids=None, owasp_category=None, exploit_poc=None, remediation_code=None):
278
+ if id: self.id = id
279
+ self.scan_id = scan_id
280
+ self.title = title
281
+ self.severity = severity
282
+ self.category = category
283
+ self.description = description
284
+ self.remediation = remediation
285
+ self.cvss_score = cvss_score
286
+ if detected_at: self.detected_at = detected_at
287
+ self.evidence = evidence
288
+ self.payload = payload
289
+ self.request_details = request_details
290
+ self.response_details = response_details
291
+ self.cwe_ids = cwe_ids
292
+ self.owasp_category = owasp_category
293
+ self.exploit_poc = exploit_poc
294
+ self.remediation_code = remediation_code
295
+
296
+ class DemoBooking(db.Model):
297
+ __tablename__ = 'demo_bookings'
298
+ def __init__(self, **kwargs): super().__init__(**kwargs)
299
+
300
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
301
+ email = db.Column(db.String(255), nullable=False)
302
+ company_size = db.Column(db.String(100), nullable=False)
303
+ meeting_date = db.Column(db.String(100), nullable=False)
304
+ meeting_time = db.Column(db.String(50), nullable=False)
305
+ status = db.Column(db.String(50), nullable=False, default='pending')
306
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
307
+
308
+ def __init__(self, email, company_size, meeting_date, meeting_time, status='pending', id=None):
309
+ if id: self.id = id
310
+ self.email = email
311
+ self.company_size = company_size
312
+ self.meeting_date = meeting_date
313
+ self.meeting_time = meeting_time
314
+ self.status = status
315
+
316
+ def to_dict(self):
317
+ return {
318
+ 'id': self.id,
319
+ 'email': self.email,
320
+ 'company_size': self.company_size,
321
+ 'meeting_date': self.meeting_date,
322
+ 'meeting_time': self.meeting_time,
323
+ 'status': self.status,
324
+ 'created_at': self.created_at.isoformat() + 'Z' if self.created_at else None
325
+ }
326
+
327
+ class EmailLog(db.Model):
328
+ __tablename__ = 'email_logs'
329
+
330
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
331
+ recipient = db.Column(db.String(255), nullable=False)
332
+ subject = db.Column(db.String(255), nullable=False)
333
+ status = db.Column(db.String(50), nullable=False, default='sent')
334
+ error_message = db.Column(db.Text, nullable=True)
335
+ sent_at = db.Column(db.DateTime, default=datetime.utcnow)
336
+
337
+ def __init__(self, recipient, subject, status='sent', error_message=None, id=None):
338
+ if id: self.id = id
339
+ self.recipient = recipient
340
+ self.subject = subject
341
+ self.status = status
342
+ self.error_message = error_message
343
+
344
+ class ScheduledScan(db.Model):
345
+ __tablename__ = 'scheduled_scans'
346
+ def __init__(self, **kwargs): super().__init__(**kwargs)
347
+
348
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
349
+ org_id = db.Column(db.String(36), db.ForeignKey('organizations.id'), nullable=False)
350
+ user_id = db.Column(db.String(36), db.ForeignKey('users.id'), nullable=False)
351
+ target_url = db.Column(db.String(500), nullable=False)
352
+ scan_type = db.Column(db.String(50), nullable=False, default='Full')
353
+ frequency = db.Column(db.String(50), nullable=False, default='daily') # daily, weekly, monthly, once
354
+ schedule_time = db.Column(db.String(5), nullable=True) # HH:MM format like "20:00"
355
+ day_of_week = db.Column(db.String(20), nullable=True) # monday, tuesday, etc.
356
+ day_of_month = db.Column(db.Integer, nullable=True) # 1..31
357
+ specific_date = db.Column(db.String(20), nullable=True) # YYYY-MM-DD
358
+ is_active = db.Column(db.Boolean, default=True)
359
+ auth_headers = db.Column(db.JSON, nullable=True) # For authenticated scanning
360
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
361
+ last_run_at = db.Column(db.DateTime, nullable=True)
362
+
363
+ def __init__(self, org_id, user_id, target_url, scan_type='Full', frequency='daily', schedule_time=None, day_of_week=None, day_of_month=None, specific_date=None, is_active=True, last_run_at=None, auth_headers=None, id=None):
364
+ if id: self.id = id
365
+ self.org_id = org_id
366
+ self.user_id = user_id
367
+ self.target_url = target_url
368
+ self.scan_type = scan_type
369
+ self.frequency = frequency
370
+ self.schedule_time = schedule_time
371
+ self.day_of_week = day_of_week
372
+ self.day_of_month = day_of_month
373
+ self.specific_date = specific_date
374
+ self.is_active = is_active
375
+ self.auth_headers = auth_headers
376
+ if last_run_at: self.last_run_at = last_run_at
377
+
378
+ class AlertSettings(db.Model):
379
+ __tablename__ = 'alert_settings'
380
+ def __init__(self, **kwargs): super().__init__(**kwargs)
381
+
382
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
383
+ user_id = db.Column(db.String(36), db.ForeignKey('users.id'), nullable=False)
384
+ email_notifications = db.Column(db.Boolean, default=True)
385
+ webhook_url = db.Column(db.String(500), nullable=True)
386
+ severity_threshold = db.Column(db.String(50), default='Medium') # Low, Medium, High, Critical
387
+
388
+
389
+ class OrganizationScanQuota(db.Model):
390
+ __tablename__ = 'organization_scan_quotas'
391
+ def __init__(self, **kwargs): super().__init__(**kwargs)
392
+
393
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
394
+ org_id = db.Column(db.String(36), db.ForeignKey('organizations.id'), nullable=False)
395
+ scan_type = db.Column(db.String(50), nullable=False)
396
+ allocated_count = db.Column(db.Integer, nullable=False, default=0)
397
+ used_count = db.Column(db.Integer, nullable=False, default=0)
398
+
399
+ __table_args__ = (db.UniqueConstraint('org_id', 'scan_type', name='uix_org_scan_type'),)
400
+
401
+
402
+ class Report(db.Model):
403
+ __tablename__ = 'reports'
404
+ def __init__(self, **kwargs): super().__init__(**kwargs)
405
+
406
+ id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
407
+ scan_id = db.Column(db.String(36), db.ForeignKey('scans.id', ondelete='CASCADE'), nullable=False)
408
+ org_id = db.Column(db.String(36), db.ForeignKey('organizations.id', ondelete='CASCADE'), nullable=False)
409
+ report_type = db.Column(db.String(50), nullable=False) # pdf, executive_summary, compliance
410
+ file_path = db.Column(db.String(500), nullable=True)
411
+ generated_at = db.Column(db.DateTime, default=datetime.utcnow)
412
+
413
+
414
+
415
+