From 1a0d6201886c6fbe4872ec168439442f520c187c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 13 Jun 2026 09:48:20 +0000 Subject: [PATCH] AI --- config/__pycache__/__init__.cpython-311.pyc | Bin 159 -> 159 bytes config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 5552 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1557 -> 1557 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 679 -> 679 bytes core/__pycache__/__init__.cpython-311.pyc | Bin 157 -> 157 bytes core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 890 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes .../context_processors.cpython-311.pyc | Bin 763 -> 763 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 3072 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 3708 bytes core/__pycache__/scanner.cpython-311.pyc | Bin 0 -> 12725 bytes core/__pycache__/tests.cpython-311.pyc | Bin 0 -> 2292 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 618 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 4362 bytes core/admin.py | 11 +- core/forms.py | 49 +++ core/migrations/0001_initial.py | 37 +++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 2739 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 45 ++- core/scanner.py | 168 ++++++++++ core/templates/base.html | 42 ++- core/templates/core/index.html | 283 ++++++++--------- core/templates/core/scan_detail.html | 70 +++++ core/templates/core/scan_history.html | 60 ++++ core/tests.py | 23 +- core/urls.py | 5 +- core/views.py | 91 ++++-- static/css/custom.css | 289 ++++++++++++++++- staticfiles/css/custom.css | 294 +++++++++++++++++- 30 files changed, 1285 insertions(+), 182 deletions(-) create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/__pycache__/scanner.cpython-311.pyc create mode 100644 core/__pycache__/tests.cpython-311.pyc create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/scanner.py create mode 100644 core/templates/core/scan_detail.html create mode 100644 core/templates/core/scan_history.html diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 896bb4f1b8949c96d2f1bf743293342b70a9d0e4..42d4d91bfefe597a9ccb97579bb68ae623d2b497 100644 GIT binary patch delta 20 acmbQwIG>ScIWI340}!yO>Sj&knG66bAp{Bl delta 20 acmbQwIG>ScIWI340}#~ttjL_mGZ_Fdwgn>q diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a79890a58443ee5f65d8d558823a19e9aed..99256beacfed21a0f5d8a4a87287d0573261fee8 100644 GIT binary patch delta 21 bcmdm>y+NC2IWI340}!yO>Sk@^St1GmH^v0U delta 21 bcmdm>y+NC2IWI340}#~ttjOHRvqTgCKXe7( diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 8cf22af743397374cabf5fa358c4f52631471db9..03841a2986b7c63fed02d12782b5c4021a7452e5 100644 GIT binary patch delta 21 bcmbQrGnI#DIWI340}!yO>Sk@^;ba2Sk@^nauSj&knFs(W(*y(n delta 20 acmbQsIG2%UIWI340}#~ttjL_mGZ6qVXayYr diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a0cd478c226e501b5189550e758662c560..db43b30e82da9047728b1ea195f121cf1f91496d 100644 GIT binary patch literal 890 zcmZWnJ8#=C5GEzRBHMA%M}XEvY7}S+4UU(NMS)B$&?FtwNeiKf7h8@VE-5>8Ck^=l z+42uu6xsSqQXqrD$v~%U4%{hIk8+eC$dNqWk$2x6kN2})w+PbjH|_q667p9oIhAgN z#Q=psCx~-~Ye@$ANO<9x@FH3j^V5lmHn^5F z!R;(ph*RRFJz|$H^@&YY(dD5ZU1~(stgk%*ZnEdOk$3BYLLPAO8kJK5#G#xx2B(h6 z4X40Or^pKkU3AJzc-6Q=+R_p1vNC+GRwP2RWIO<6311#E>BSErG6^YrOfx-(sSmwuTxUZtP( z5HE7iUHpkv-gl#Jys0ZB{C%N&MU}ETOQG9HRRnbLmG~rUN0&eQ!)5sA8YzQoqI6w( zzbw-S(Lz->iX0=Go0QTiX`SDnGqQeuf6hz9c$7XnUO%z^(B=hg&W$2%&B;S{0vBr= SSNh_gW>+RLUOu$XQu_~a*Y2GF delta 168 zcmeyxc7-uzIWI340}#~ttjM$n(vLwL7+``jJ_`XE(-~42QW$d>av7r-85vTTf*CZK zUxE~9GTvfMOv%m6^V4Ly#g~$mn3tZfmzKBJiZhlH>PO4oI c2T+U=h>K-`#0O?ZM#dWq3Ky`UA~v830BFu73IG5A diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 2fa4a49f9779402d93d4c58688d65ede2fff84d5..bb6c68c990bcd6d68590e779939ec728760962e7 100644 GIT binary patch delta 20 ZcmeBS>0#kn&dbZi00eBRx>*Z(m;fhQ18D#N delta 20 acmeBS>0#kn&dbZi00ebDD>4`IFaZE9?*xJX diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf2234fb21a6b62efc5cec11af9512dd0c9616..db533da4726a4c9e7cbf0cb9e14ca2f2d3fb7595 100644 GIT binary patch delta 21 bcmey(`kR$!IWI340}!yO>Sk@^dCvp@K3xS! delta 21 bcmey(`kR$!IWI340}xbw%g@}%^PUL+NjnD4 diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0592b1fff62be57033fcd01e2235f6c62de47f0e GIT binary patch literal 3072 zcma)8-ESMm5#PJx@%u}ZOv|yNocs}))&~LC$U|c!ZVcNM(zt>WvoP6}kT z9L?^|&d$#KX7>J?NJJ5|f1I9MITb?aKVr~tYLD6fEil`NAR?KlEEOb4@UkhFm4f2& ziWw-Y1r_)JQO#gER0v5(MpqFD-a{lrrIm1B>0MDlhB?Vqmn>H1 zz{GD}|HZGcX%K8%492P6z_2Qp8MD|6$=?vYQDPL^i$z>1`YL@I=?(U~!2BMpr>Gzi zRFGvvm6bqWl&UMiKEDF@_ZvtI>>ZNmYXwg{l^=taCj(7|sHZvB*Fh8#@MhwJ8I=S{ z=xh0D_%Rs&L|njYPa|*nB;oB#_SZ?2#7LYZo+jTqF-21MP$AtyOA<-n3l~OOXjLj? zz@H;-^1v@wz@?32zb;{0BbK&i)wDXU*qUu=9K7~%F5rfWCCeyM?gr}yS*ErdGAf2` zVAEAie2bcmEJR~gqh)NEvzo=UGUXgEQ=>JC&uVWSN8tY^d0_8P6#!0hYMYGG5h9T3NPBOhn_#jmFF(zDu<#Gw$Nz+8oDA6il!& zU<+#iIIX;<5zI@sEX+tv!04A16HeE7$zpIi7S0fsRT;f&(E6-a0_j|@}b3`Xmc-A`6`NLUCg9pQ^|?tgry5L92Gpu8m;3+&+Ij&NH;)%Vu0n|}Lg1?0 zW=7RjAZSdOCgRMgxx}(C;4%U$z<^L-w565y{Y0CDX2qJV)99IT7x$T|Fs!fGen##^0=>tQC zZ?hg~26hf1=s@mmEOC{9?4Qv$@_HZwkA^*RAUmwH85p))LX{IIk)1h^KZV$lb=8Kd z)Ne!`-qTdSlnGi_zfkK^9jz+AL3ODaXi7xB09!m%2d}viQ5ki6tx7!%=Hy1GWZTuB z=JRjDg5kh14K_4By6iz6Yj?$C<(E9DD7>aTf@-#p-c~7i} z+;JnIf`~U1l+e|p8`1Tmi8kPPG=C(TQ(Ir=O2bx6gdMJ9@D*da;Y7#7~@wb31G8 ziF435BVAcZ+>xC0@F6_o9z3k<}#m_QFKRJt@osC|KD9_HPF9no;22|j4qi$H& zE4WN`-Hqyc*&;Pl@Nr$gQ^Tge!bGmSu}j`_dc~k7@nXq@2buWLa%0yqF|0*u(xS~y zi)j%pH*%?jnYY0W-+89UOI(ZtW>uBx1lc9Bogv6o0m$6g82 z4TVym2*@izHm-KlD0^n}X6MMXGm>@EV@~ozCsT*>vf{j~I4>LAxYkVu)T3QAAn}A4 z{y4(k?ce?yq+F1!;L5V)M=R?O=EeIl?`5(0bgn(zzeAJl;oc4A WB=E%d$>zs#>IW7*BmV~@JNVQ9 literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5fb7c2b603c587f8162d643eb10596d2858..47b0db3de99c886ca18e55d3590bd585ea0a21fc 100644 GIT binary patch literal 3708 zcma)9O>Eo96{bW{f0kwaI70!I16+n zKE8SL=FOWo?|mcvVQ|pTK>Op=-L*Ln!~BPK+Kp>5Z|8vdl0gh|NKD01a5yOMl$;f| zz&aQwGtVIQ1%q7JvF2(|e$6t>8+hzi;1IXMB;6;pn{=4&ih{7D0_WAl3f@sz`SX9m_o!FjkX0i2L4C-{Ez=1I>UW_L{aQ8-e>2MrE|Hx7Od1C zhY?F-w5@@!tc|sK&>rrychx%cOOP3t`$n#zkrzziI!>U`7fj!d$I%r?$}!WQFB2?i zD@8%>$-?%jyd8$6J6VVv1r|9AF2urY*IBePJ4H?8x7TsfX|h!!?F@eUWHFIWe1=z5 zQKM2%1*@u1!aG9`D}pG&*pe;CB{|c}^RiIEJa77W9x9|NQ9i))zpM&UYlH=hrf2El z%F68g!>^gQG$?1ObXlq3bak~VYt=N~!o{klko2ZPo~i3X5vPj^!D+kZQtR8Mx9umP zDes0yrDst)|1^S=yKEzp+V#*6p(#UXX1qT<*I?v*;eo1m72I~^pT)Yu^h8vjE#eI< z(Kt!UrpYcUo4~s(7>U)2>0YwlAaRw&QrUDp1`0iIk%*dD6eQF8pvC2q9x_1PqeVpq zfq=2)&%ngPx$LJ)Ff}{>7}TF+^VtWpizGzVto=l&uJ7Cx5{EtoL-hub?of|LgX23GCDsYv z5bvuP%TT57)tqCLvnmuXY0s(DrKL^sC1|_HysAAVz8_fZ?5nFN|x_WV` zPlqcx`ZY>UNFz5?o9Aq(D~*=0ChQE{Ez;t}h!-@|r$Pbp8dRX^hn+lwytESUWG@U2hx=`vGKi+cD?@& zT;6^3a&iC4futv9^gyN_$QXf4qa_~O59)z*J&-m6=_bcU{oMukJup~oV-WBRy{)KY ztw?{t>_g{b>qWV*;M)FzSv$7_eJAwK?=}RN_ivmd=VrcQeowDzSFTDAHu2X#C#n=A z($n|u(ay=Yp_63IcnNF#d9-;Xs_T-l-HN(O@x}~7bf{J<<-c~AE)`2pRq9HDSmo#8 zc~v7kzZ2;?)|QMWy*dM=#xx?s|A^fAd*n_%k}@JGeW(QAqq#pV+uYys#?miz0qLs< zsIi1?vFkuYmcIfk?pHZk-re5o?RPV?`s^kyATXLu`+n2F{kdMA!a# z_P3?<4f+&zt%kVXI}4)k&Y2#yS_A-5P0yx4WXMU=jRd)b385DuWI6`8KmyqY0Hi2H zC<>G7lq4t_2V$}ie=<$GKLFAl`H)o}s{$%vz9J)VMQ3M$)R?B584SP7 z8G&p2zD6KKx$*rETio^i%PnqVztHq?A^(5<%s}wB6T9f8^6FQ0{|AQugXRF|yJMY9 zE_tI5yzRmp$OFnfuo2X{cdQ+nz6WK2JfekcW7<8su?!G1(1K3BEU0C3%GJX%_NJ^R8#*2_0_?lrqju#o6JSB zci)Ikozj7Kol&ogyWM0ilD$Vp?B*#Qc-I*XFyZk%w-HJld~)nHzW-PcW$U4=5z5xG zjri2wPmK7~QB?o<*LwVUJ^tK?Kd;)1$NX=&TW)tt~df(|f~4c=~u; z4`=G(j1kV%78+n3gyRQKjz^3S7xetkjl7_TSL@+bBfJXZ!E3uujo`I|2|bvq2UA8c zRhw@_ZtUGOA~))hNh2~@`_Z|P$s?qPXX@b@BRm6hM{mEnX^h@J`tGr&kIvOc=Zw)g z-F2xEo1|NvJaX!poE}@Q$Ci!Qa&4&*zeV?R>nNtj@7Lq^jre`oOn8bqICW4yl>U;} z!wdEBf)QS*Ei_#m=Wj9>$tjh+>kI@PT-niNx~Q9R2b)eecc;k!Ic1^y;-?*fCAEKf z&_mB9-KGz%0W2sfv})H*pw+KxqNEZlqqrsdZ_7@T-qbnsX6)aYKO{c`dlZ>f9!QgQ lI2;XTOz-_Rm~p-L+jJ)#;Oj-QH~hP?Q!0P=oav7r-85vTTf*CZK zUxE~9GTvg#%}+_qDfZK3y2Y82m6(^Fua}Zk#0-?2yr0?J>KBI%P(f)d0X Xi)Det2WCb_#v2SW7qFosHlPXs(J~|F diff --git a/core/__pycache__/scanner.cpython-311.pyc b/core/__pycache__/scanner.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d777069004f7fd5c2a8bf7014555fbbbb67a9825 GIT binary patch literal 12725 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 It=PqX0EfXz%>V!Z literal 0 HcmV?d00001 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988a223abc7b5af333902925bc196186bbf5..313472c2ce42a1f3a275e0b982087c6c3718226e 100644 GIT binary patch literal 618 zcmah_zfaph6n+=Ka~w>F+69CvW$F;MU@20T)`F!QDodHXAlG*y0iPY6Z33AX8JP;* zm^F}RrWBbv*su0EZrLW4pBG^^Zuxs7cqF0VV`@ZCQq7HRgELQ>uNb%Hi9bm4cf*4XPu4ZN-Eox5_QPZ^ zT`y#uNi(schXX#!Dv!#%Dy>|UtE+UcIhRTH9qx2B+`siqk6!V)NbTg)z{1-gxtWZ^6CB!o8|xZ%m&&x`EdI3 zbmzh@u|2`|6x%oGXm0uJ&E>}uyA$kAu{-Bh&iu=y#NGsZQ|wh-*;u{Ux)vq+6ZEI( P&lv delta 255 zcmaFGa+|4sIWI340}#~ttjNp)(vLwL7+{4mKHC5p(-~42QW$d>av7r-85vTTQkZj? za+#x;85x)uQW;ZNQkhd&*RU;PW?)zi#1N3q7{!vp9?YQ0@e(AU$#{#UAh9IlB_ouR zVUVAjdW)e5WD!U*FEKaOPm}c)cS=@bUV6S>X;Dsb5y-S#tYw+0<;6v;lk*t^xcGn~ zAR~&sCZA-KF%!SQfPy}-F|cwrxO510gv?;Oz#@N*WY6bwT CB{<~( diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c7137f7930a63ea76fcd63516c4974e834..05874c960090386d4df56d26e1c32e4c7701f939 100644 GIT binary patch literal 4362 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 Xr9HlS=yH2}4P;gNa)91vejfh;`i@)& diff --git a/core/admin.py b/core/admin.py index 8c38f3f..dcdf5b7 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,12 @@ from django.contrib import admin -# Register your models here. +from .models import ThreatScan + + +@admin.register(ThreatScan) +class ThreatScanAdmin(admin.ModelAdmin): + list_display = ("id", "scan_type", "risk_score", "risk_level", "target_preview", "model_version", "created_at") + list_filter = ("scan_type", "risk_level", "model_version", "created_at") + search_fields = ("target_preview", "content_hash", "verdict") + readonly_fields = ("created_at", "content_hash") + ordering = ("-created_at",) diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..2197ec4 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,49 @@ +from django import forms +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError + +from .models import ThreatScan + + +class ThreatScanForm(forms.Form): + scan_type = forms.ChoiceField( + choices=ThreatScan.ScanType.choices, + widget=forms.RadioSelect, + initial=ThreatScan.ScanType.URL, + label="What do you want to scan?", + ) + content = forms.CharField( + label="URL, email, or message", + max_length=5000, + widget=forms.Textarea(attrs={ + "rows": 6, + "placeholder": "Paste a suspicious URL, email, SMS, or chat message. Raw text is analyzed in-memory and not stored.", + }), + ) + store_metadata = forms.BooleanField( + required=False, + initial=True, + label="Save privacy-safe metadata for my dashboard", + help_text="Only a short sanitized preview, hash, score, and explanation are stored — not the raw submission.", + ) + + def clean_content(self): + content = self.cleaned_data["content"].strip() + if len(content) < 6: + raise ValidationError("Please enter enough text to analyze.") + return content + + def clean(self): + cleaned = super().clean() + scan_type = cleaned.get("scan_type") + content = cleaned.get("content") + if scan_type == ThreatScan.ScanType.URL and content: + candidate = content.strip() + if not candidate.startswith(("http://", "https://")): + candidate = f"https://{candidate}" + try: + URLValidator()(candidate) + except ValidationError as exc: + raise ValidationError("Enter a valid URL, or switch the scan type to Email / Message.") from exc + cleaned["content"] = candidate + return cleaned diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..f049a2e --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.7 on 2026-06-13 09:41 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ThreatScan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('scan_type', models.CharField(choices=[('url', 'URL / Website'), ('message', 'Email / Message')], max_length=20)), + ('target_preview', models.CharField(help_text='Sanitized preview only; raw sensitive content is not stored.', max_length=220)), + ('content_hash', models.CharField(db_index=True, max_length=64)), + ('risk_score', models.PositiveSmallIntegerField(default=0)), + ('risk_level', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], default='low', max_length=20)), + ('verdict', models.CharField(max_length=160)), + ('explanation', models.TextField()), + ('indicators', models.JSONField(blank=True, default=list)), + ('recommended_actions', models.JSONField(blank=True, default=list)), + ('model_version', models.CharField(default='heuristic-nlp-v1', max_length=40)), + ('store_metadata', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ], + options={ + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['scan_type', 'risk_level'], name='core_threat_scan_ty_620347_idx'), models.Index(fields=['created_at'], name='core_threat_created_e17dbf_idx')], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..539092f5e75c921bb7965fd0c10ba3e2b21fe479 GIT binary patch literal 2739 zcma)8J!~6C7TzEJ>&K*RS?8D*SCZpUjwR|RkbQ_Q{!^TYk}N|~kbsD9Z^;=gS6c2e zyG!aP0pj9{E7P5d;2>O~O&wNcfmdAB{*fQ89E!^9Vo79=DVWL+qPBmsxlBb^73gk-Qp*JH0NA`(#ug&UFm5Yz#8aIHxw%8~Jo z1;tQYmr$b7wGWw|!Tk!^eG1uwdK>4?ppvIhB1$#RpFv&VDBf!pA6;zp?Te5P?%;yT zE}e{|3#k9!BKi~5cKKvC0sGELEV|N2pNV&sW4byS_Ca~pnFtVLfcv{UCM4pXM&?ZA z${#`vo~maE4L7cxiS;f=^}f#dmM&c9m~*c&IJutc%+}Ap(k^tg#(Ssp=shT_i#$$; zM*@nXOR&!)jTuHXTJ+z%I@Sp&sv`va_ncE*Kj<%_|*n(t69V;c<^04J8 zhO1b%r?{T&U^I68Ymg~k#%5LZ@RsMWy`kN0xLVfS^6}jO6hlSTun^t~FbT(SH&nL- zfgE=9MnoD#culLB9u0gZu48i~#2R4Qn^az~H$jh7FfwWt8d>BcSVCn(FVoPcAYm3O zIZ$#*GimHWTXU|m__D(op`kk3f^Wt{B5Q}SQp-IJIOMx!6KoUQOm53)+dg$ERpm^3BA0L zY6W{5(mahO+AL8u4{(=RsjS|euF=;HI>36SdYo&}K+Y3&dUAZ~gIlVBwpfqW3~VBo zhB%i_;5%{f#4S`@YvW_KgMf&ZPNRH>unXOqVA;n((5PMI3w$(kiN;h;P*tS~RfYA| zObmLrs(w||OqLF25CpaWcAy*KDr=EdGO+8gPXg_Fz|X-Vd+$5!; zm`2dLX6ZH!*F3{?Y0Lx9j%{HoTR^e!9OEZV*j3#ALj6a00W}K0-^rEj3eMGvHOs5z z@D?sX#ZGS1b~fCqR>C>9C%M;Ow%qvm_yo|M_hGEM?XWKv7x1YS+#)dk>8sywz2~2P z(h7+eBTpAvF(ID$N5YQ{9Hp-Qmg*z@H@>U;{WlImhu26!C6+__-Dbb*_q!zN9i`G_ zU~G5I9~dLIACu)Ox%E{`5WW@fNibyYOV7D{$t74)52T~iAQ_(8oA!sN$VcCREiWxH z@U=$_3=+T~8F&f688SGrcgY`|IQZc3J{eqU4lenFOCd^3~x zGkKDn=Z+k628c4g7x9(xrZVX(lO#FyM!uiW&$A>m*UZfMnK_coAEmC7?Cl>Te)jf( zceqNj%gyYvpIs)&m7|nGh9>u}`$LoD!$nf~k_R|P7`j;7h{%`&~A{pGwU_XONa;`BF08XI-j_)2Ygjkaz%Va;&W6171shc9TQ=0iyD~J04IFt_ z24ni3+zQKmy)A)>;l6m{xd7rN6D_&_eB^1K{hav@=|&>7FJS%{C;nK6_s%RpXuR!1 zMSe*6xeOSaXAyXq1<-zv%>+mOBWDSw>^~T{2%;s6qS%TE;#DG~gTEsoOWyX^iVTR6 VmT*>lm++ISj&knF9bTfCMD~ delta 20 acmZ3%xPp;qIWI340}#~ttjL_mGY0@N6$L&3 diff --git a/core/models.py b/core/models.py index 71a8362..eaab6f0 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,46 @@ from django.db import models +from django.utils import timezone -# Create your models here. + +class ThreatScan(models.Model): + class ScanType(models.TextChoices): + URL = "url", "URL / Website" + MESSAGE = "message", "Email / Message" + + class RiskLevel(models.TextChoices): + LOW = "low", "Low" + MEDIUM = "medium", "Medium" + HIGH = "high", "High" + CRITICAL = "critical", "Critical" + + scan_type = models.CharField(max_length=20, choices=ScanType.choices) + target_preview = models.CharField(max_length=220, help_text="Sanitized preview only; raw sensitive content is not stored.") + content_hash = models.CharField(max_length=64, db_index=True) + risk_score = models.PositiveSmallIntegerField(default=0) + risk_level = models.CharField(max_length=20, choices=RiskLevel.choices, default=RiskLevel.LOW) + verdict = models.CharField(max_length=160) + explanation = models.TextField() + indicators = models.JSONField(default=list, blank=True) + recommended_actions = models.JSONField(default=list, blank=True) + model_version = models.CharField(max_length=40, default="heuristic-nlp-v1") + store_metadata = models.BooleanField(default=True) + created_at = models.DateTimeField(default=timezone.now, db_index=True) + + class Meta: + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["scan_type", "risk_level"]), + models.Index(fields=["created_at"]), + ] + + def __str__(self): + return f"{self.get_scan_type_display()} · {self.risk_score}/100 · {self.target_preview[:50]}" + + @property + def risk_badge_class(self): + return { + self.RiskLevel.LOW: "success", + self.RiskLevel.MEDIUM: "warning", + self.RiskLevel.HIGH: "danger", + self.RiskLevel.CRITICAL: "critical", + }.get(self.risk_level, "secondary") diff --git a/core/scanner.py b/core/scanner.py new file mode 100644 index 0000000..5cc1c39 --- /dev/null +++ b/core/scanner.py @@ -0,0 +1,168 @@ +import hashlib +import math +import re +from dataclasses import dataclass +from urllib.parse import urlparse + +from .models import ThreatScan + +SUSPICIOUS_TLDS = {"zip", "mov", "click", "country", "gq", "tk", "ml", "cf"} +BRAND_TERMS = {"paypal", "microsoft", "google", "apple", "amazon", "bank", "chase", "wellsfargo", "office365"} +URGENCY_TERMS = {"urgent", "immediately", "verify", "suspended", "locked", "limited", "expire", "password", "invoice", "wire", "gift card", "crypto"} +CREDENTIAL_TERMS = {"login", "signin", "sign in", "password", "2fa", "otp", "account", "credentials", "ssn"} +URL_SHORTENERS = {"bit.ly", "tinyurl.com", "t.co", "goo.gl", "ow.ly", "is.gd", "buff.ly", "cutt.ly"} + + +@dataclass +class ScanResult: + risk_score: int + risk_level: str + verdict: str + explanation: str + indicators: list[dict] + recommended_actions: list[str] + target_preview: str + content_hash: str + + +def _hash_content(content: str) -> str: + return hashlib.sha256(content.encode("utf-8")).hexdigest() + + +def _preview(content: str, limit: int = 180) -> str: + clean = re.sub(r"\s+", " ", content).strip() + clean = re.sub(r"([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@)", r"•••", clean) + return clean[:limit] + ("…" if len(clean) > limit else "") + + +def _add(indicators: list[dict], label: str, weight: int, detail: str): + indicators.append({"label": label, "weight": weight, "detail": detail}) + + +def _risk_level(score: int) -> str: + if score >= 85: + return ThreatScan.RiskLevel.CRITICAL + if score >= 65: + return ThreatScan.RiskLevel.HIGH + if score >= 35: + return ThreatScan.RiskLevel.MEDIUM + return ThreatScan.RiskLevel.LOW + + +def _verdict(score: int) -> str: + if score >= 85: + return "Likely malicious — isolate and do not interact" + if score >= 65: + return "High-risk suspicious content" + if score >= 35: + return "Needs review before trusting" + return "Low risk based on current signals" + + +def _actions(level: str) -> list[str]: + if level in {ThreatScan.RiskLevel.CRITICAL, ThreatScan.RiskLevel.HIGH}: + return [ + "Do not click links, download attachments, or enter credentials.", + "Report this item to your security team or service provider.", + "If you already interacted, rotate passwords and review account activity.", + ] + if level == ThreatScan.RiskLevel.MEDIUM: + return [ + "Verify the sender/domain through an independent channel.", + "Hover or inspect links before opening them.", + "Avoid sharing credentials or payment details until confirmed.", + ] + return [ + "No strong malicious signals were found, but continue to verify unexpected requests.", + "Keep software and browser protections enabled.", + ] + + +def scan_content(scan_type: str, content: str) -> ScanResult: + indicators: list[dict] = [] + score = 5 + lowered = content.lower() + + if scan_type == ThreatScan.ScanType.URL: + parsed = urlparse(content) + host = (parsed.netloc or parsed.path).lower().split(":")[0] + path = parsed.path.lower() + labels = [part for part in host.split(".") if part] + tld = labels[-1] if labels else "" + + if parsed.scheme == "http": + score += 18 + _add(indicators, "Unencrypted HTTP", 18, "The URL uses http:// instead of https://.") + if host.replace(".", "").isdigit() or re.match(r"^\d+\.\d+\.\d+\.\d+$", host): + score += 22 + _add(indicators, "IP address host", 22, "Phishing links often hide behind raw IP addresses.") + if host in URL_SHORTENERS: + score += 20 + _add(indicators, "Shortened URL", 20, "Shorteners hide the final destination until opened.") + if tld in SUSPICIOUS_TLDS: + score += 14 + _add(indicators, "Higher-risk TLD", 14, f".{tld} domains are frequently abused in commodity phishing.") + if len(host) > 38 or len(content) > 120: + score += 10 + _add(indicators, "Long destination", 10, "Very long hosts/URLs can hide deceptive tracking or redirect chains.") + if "@" in content: + score += 18 + _add(indicators, "@ symbol in URL", 18, "The @ character can disguise the actual destination host.") + if sum(1 for ch in host if ch == "-") >= 2: + score += 8 + _add(indicators, "Hyphen-heavy domain", 8, "Multiple hyphens can imitate legitimate brand domains.") + matched_brands = [brand for brand in BRAND_TERMS if brand in host and not host.endswith(f"{brand}.com")] + if matched_brands: + score += 16 + _add(indicators, "Brand impersonation pattern", 16, f"The host contains sensitive brand terms: {', '.join(matched_brands[:3])}.") + if any(term in path for term in CREDENTIAL_TERMS): + score += 12 + _add(indicators, "Credential-themed path", 12, "The path references login, password, or account actions.") + else: + urgent_hits = [term for term in URGENCY_TERMS if term in lowered] + credential_hits = [term for term in CREDENTIAL_TERMS if term in lowered] + money_hits = re.findall(r"\$\s?\d+|wire transfer|gift card|bitcoin|crypto", lowered) + url_count = len(re.findall(r"https?://|www\.", lowered)) + + if urgent_hits: + weight = min(25, 8 + len(urgent_hits) * 4) + score += weight + _add(indicators, "Urgency and pressure language", weight, f"Found terms such as {', '.join(urgent_hits[:5])}.") + if credential_hits: + weight = min(24, 10 + len(credential_hits) * 3) + score += weight + _add(indicators, "Credential request", weight, f"The message asks about {', '.join(credential_hits[:5])}.") + if money_hits: + score += 16 + _add(indicators, "Payment or transfer request", 16, "The message references money movement or irreversible payments.") + if url_count: + score += min(20, url_count * 7) + _add(indicators, "Embedded link", min(20, url_count * 7), f"Detected {url_count} link-like item(s) in the message.") + if re.search(r"dear (customer|user|client)|kindly|act now|final notice", lowered): + score += 10 + _add(indicators, "Common scam phrasing", 10, "The wording resembles common phishing templates.") + if len(content) < 40 and any(term in lowered for term in ["click", "verify", "login"]): + score += 8 + _add(indicators, "Sparse context", 8, "Short messages with action links are harder to verify safely.") + + # Normalize so a pile-up of weak signals does not instantly max out risk. + score = min(100, max(0, round(100 * (1 - math.exp(-score / 85))))) + if not indicators: + _add(indicators, "No strong threat indicators", 0, "The scanner did not find obvious phishing markers in this sample.") + + level = _risk_level(score) + explanation = ( + "This first MVP uses a local heuristic/NLP-style rules engine designed to be replaced or blended " + "with a trained Scikit-learn model. It does not store the raw submission; the dashboard saves only " + "a sanitized preview, SHA-256 hash, score, and explanation." + ) + return ScanResult( + risk_score=score, + risk_level=level, + verdict=_verdict(score), + explanation=explanation, + indicators=sorted(indicators, key=lambda item: item["weight"], reverse=True), + recommended_actions=_actions(level), + target_preview=_preview(content), + content_hash=_hash_content(content), + ) diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..6875c2d 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,11 +1,13 @@ +{% load static %} - {% block title %}Knowledge Base{% endblock %} + + {% block title %}{{ project_name|default:"SentinelAI Cyber Assistant" }}{% endblock %} + {% if project_description %} - {% endif %} @@ -13,13 +15,45 @@ {% endif %} - {% load static %} + + + + {% block head %}{% endblock %} - {% block content %}{% endblock %} + + +
+ {% block content %}{% endblock %} +
+
+
+ SentinelAI Cyber Assistant · Explainable risk scoring + Privacy-first MVP: raw submissions are analyzed in-memory only. +
+
+ diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..afcd0ad 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,150 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}SentinelAI Cyber Assistant — Phishing & Scam Risk Scanner{% endblock %} +{% block meta_description %}Paste a URL, email, or message to receive a privacy-safe 0–100 phishing risk score with clear explanations.{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+ + +
+
+
+ Privacy-first cyber defense +

Detect phishing URLs and scam messages before they reach people.

+

SentinelAI gives every submission a 0–100 risk score, explains the strongest signals, and stores only privacy-safe metadata for your dashboard.

+ +
+ URL phishing detection + Scam message NLP + Explainable AI +
+
+
+
+
+ + Live risk console +
+
+ {{ avg_score }} + avg risk +
+
+
{{ total_scans }}Scans
+
{{ high_risk_count }}High risk
+
v1Model
+
+
+
Local in-memory analysis
+
False-positive review dashboard
+
Threat explanations included
+
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file + + +
+
+
+
+
+
First workflow
+

Scan a suspicious URL, email, or message

+

This MVP uses a local heuristic/NLP engine as the safe baseline before adding trained Scikit-learn models and public cybersecurity datasets.

+
+ {% csrf_token %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} +
+ {{ form.scan_type.label }} +
+ {% for radio in form.scan_type %} + + {% endfor %} +
+ {% for error in form.scan_type.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.content }} + {% for error in form.content.errors %}
{{ error }}
{% endfor %} +
+ + +
+
+
+
+
+
Security posture
+

Built for sensitive submissions

+
+
+ 01 +

Raw content stays out of storage

+

Only a sanitized preview, SHA-256 hash, risk score, and explanation are persisted.

+
+
+ 02 +

Explainable decisions

+

Every result lists weighted indicators so users can understand what triggered the score.

+
+
+ 03 +

ML-ready pipeline

+

The scanner interface is ready to blend in trained models, evaluation metrics, and safe updates.

+
+
+
+
+
+
+
+ +
+
+
+
+
Threat dashboard
+

Recent detections

+
+ Open full history +
+ {% if recent_scans %} + + {% else %} +
+ +

No scans yet

+

Run your first URL or message scan to populate the dashboard.

+ Start scanning +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/scan_detail.html b/core/templates/core/scan_detail.html new file mode 100644 index 0000000..c39654b --- /dev/null +++ b/core/templates/core/scan_detail.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% block title %}Scan Result #{{ scan.pk }} — SentinelAI{% endblock %} +{% block meta_description %}Explainable AI cybersecurity result with risk score, flagged indicators, and recommended next actions.{% endblock %} + +{% block content %} +
+
+
+
+ Scan confirmation +

{{ scan.verdict }}

+

Result created {{ scan.created_at|date:"M j, Y H:i" }} using {{ scan.model_version }}.

+
+ Scan another item +
+
+
+
+
+
+
+
+
+ {{ scan.risk_score }} + risk / 100 +
+ {{ scan.get_risk_level_display }} risk +
+
Scan type
{{ scan.get_scan_type_display }}
+
Sanitized preview
{{ scan.target_preview }}
+
Content hash
{{ scan.content_hash|slice:":16" }}…
+
+
+
+
+
+
Why it was flagged
+

Explainable indicators

+

{{ scan.explanation }}

+
+ {% for indicator in scan.indicators %} +
+
+ {{ indicator.label }} +

{{ indicator.detail }}

+
+ +{{ indicator.weight }} +
+ {% endfor %} +
+
+
+
Recommended response
+

Next actions

+
    + {% for action in scan.recommended_actions %} +
  • {{ action }}
  • + {% endfor %} +
+ +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/scan_history.html b/core/templates/core/scan_history.html new file mode 100644 index 0000000..d42bebe --- /dev/null +++ b/core/templates/core/scan_history.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} + +{% block title %}Threat Dashboard — SentinelAI{% endblock %} +{% block meta_description %}Review privacy-safe phishing and scam scan results by risk level, score, and explanation.{% endblock %} + +{% block content %} +
+
+ Dashboard +

Threat scan history

+

Monitor risk levels, review explanations, and identify items that need human verification.

+
+
+
+
+
+
Total scans{{ total_scans }}
+
Average risk{{ avg_score }}/100
+
High-risk items{{ high_risk_count }}
+
+
+ {% if scans %} +
+ + + + + + + + + + + + + {% for scan in scans %} + + + + + + + + + {% endfor %} + +
RiskTypePreviewVerdictCreated
{{ scan.risk_score }}/100{{ scan.get_scan_type_display }}{{ scan.target_preview }}{{ scan.verdict }}{{ scan.created_at|date:"M j, Y H:i" }}Review
+
+ {% else %} +
+ +

No detections yet

+

Use the scanner to create your first privacy-safe result.

+ Run first scan +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/tests.py b/core/tests.py index 7ce503c..9a14e48 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,24 @@ from django.test import TestCase +from django.urls import reverse -# Create your tests here. +from .models import ThreatScan +from .scanner import scan_content + + +class ThreatScanWorkflowTests(TestCase): + def test_scanner_flags_suspicious_url(self): + result = scan_content("url", "http://paypal-login-security.example.click/account/verify-password") + self.assertGreaterEqual(result.risk_score, 35) + self.assertTrue(result.indicators) + + def test_post_scan_creates_privacy_safe_record_and_redirects(self): + response = self.client.post(reverse("create_scan"), { + "scan_type": "message", + "content": "Urgent: verify your password now or your bank account will be suspended. Click https://example.com", + "store_metadata": "on", + }) + self.assertEqual(response.status_code, 302) + scan = ThreatScan.objects.get() + self.assertNotIn("Urgent:", scan.content_hash) + self.assertGreater(scan.risk_score, 0) + self.assertTrue(scan.explanation) diff --git a/core/urls.py b/core/urls.py index 6299e3d..dd28be8 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path -from .views import home +from .views import create_scan, home, scan_detail, scan_history urlpatterns = [ path("", home, name="home"), + path("scan/", create_scan, name="create_scan"), + path("scans/", scan_history, name="scan_history"), + path("scans//", scan_detail, name="scan_detail"), ] diff --git a/core/views.py b/core/views.py index c9aed12..fab6f6c 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,80 @@ -import os -import platform +from django.db.models import Avg, Count, Max +from django.shortcuts import get_object_or_404, redirect, render -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +from .forms import ThreatScanForm +from .models import ThreatScan +from .scanner import scan_content + + +def _dashboard_context(): + scans = ThreatScan.objects.all() + totals = scans.aggregate(total=Count("id"), avg_score=Avg("risk_score"), latest=Max("created_at")) + high_risk_count = scans.filter(risk_level__in=[ThreatScan.RiskLevel.HIGH, ThreatScan.RiskLevel.CRITICAL]).count() + return { + "total_scans": totals["total"] or 0, + "avg_score": round(totals["avg_score"] or 0), + "latest_scan_at": totals["latest"], + "high_risk_count": high_risk_count, + "recent_scans": scans[:6], + } def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() - + form = ThreatScanForm() context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "project_name": "SentinelAI Cyber Assistant", + "meta_description": "Privacy-first AI cybersecurity assistant for phishing URL and scam message risk scoring with clear explanations.", + "form": form, + **_dashboard_context(), } return render(request, "core/index.html", context) + + +def create_scan(request): + if request.method != "POST": + return redirect("home") + form = ThreatScanForm(request.POST) + if not form.is_valid(): + context = { + "project_name": "SentinelAI Cyber Assistant", + "meta_description": "Scan a suspicious URL, email, or message for phishing risk.", + "form": form, + **_dashboard_context(), + } + return render(request, "core/index.html", context, status=422) + + result = scan_content(form.cleaned_data["scan_type"], form.cleaned_data["content"]) + scan = ThreatScan.objects.create( + scan_type=form.cleaned_data["scan_type"], + target_preview=result.target_preview, + content_hash=result.content_hash, + risk_score=result.risk_score, + risk_level=result.risk_level, + verdict=result.verdict, + explanation=result.explanation, + indicators=result.indicators, + recommended_actions=result.recommended_actions, + store_metadata=form.cleaned_data["store_metadata"], + ) + return redirect("scan_detail", pk=scan.pk) + + +def scan_history(request): + scans = ThreatScan.objects.all() + context = { + "project_name": "Scan History", + "meta_description": "Review privacy-safe cybersecurity scan results and risk levels.", + "scans": scans, + **_dashboard_context(), + } + return render(request, "core/scan_history.html", context) + + +def scan_detail(request, pk): + scan = get_object_or_404(ThreatScan, pk=pk) + context = { + "project_name": f"Scan Result #{scan.pk}", + "meta_description": "Detailed phishing and scam risk result with explainable AI indicators.", + "scan": scan, + } + return render(request, "core/scan_detail.html", context) diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..1881a0c 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,287 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* SentinelAI custom theme */ +:root { + --bg: #06111f; + --bg-2: #0b1f33; + --surface: rgba(15, 34, 54, 0.82); + --surface-strong: #10263d; + --line: rgba(163, 205, 255, 0.16); + --text: #eef7ff; + --muted: #9fb6ca; + --primary: #10e5b9; + --primary-dark: #08a98a; + --secondary: #28c2ff; + --accent: #ffb020; + --danger: #ff4d6d; + --success: #2ee59d; + --shadow: 0 24px 80px rgba(0, 0, 0, 0.35); + --radius-xl: 28px; + --radius-lg: 20px; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: + radial-gradient(circle at 10% 5%, rgba(16, 229, 185, 0.16), transparent 28rem), + radial-gradient(circle at 88% 20%, rgba(40, 194, 255, 0.14), transparent 24rem), + linear-gradient(135deg, var(--bg), #081827 54%, #03101d); + color: var(--text); + min-height: 100vh; +} + +h1, h2, h3, .navbar-brand { + font-family: "Space Grotesk", "Inter", sans-serif; + letter-spacing: -0.04em; +} + +a { color: inherit; } + +a:hover { color: var(--primary); } + +.skip-link { + position: absolute; + left: -999px; + top: 0; + background: var(--primary); + color: #03101d; + padding: .75rem 1rem; + z-index: 9999; +} + +.skip-link:focus { left: 1rem; top: 1rem; } + +.app-nav { + background: rgba(6, 17, 31, 0.78); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(18px); +} + +.navbar-brand { color: var(--text); font-weight: 700; } +.navbar-brand:hover, .nav-link:hover { color: var(--primary); } +.nav-link { color: var(--muted); font-weight: 600; } +.navbar-toggler { border-color: var(--line); } +.navbar-toggler-icon { filter: invert(1); } +.brand-mark { color: var(--primary); text-shadow: 0 0 24px rgba(16, 229, 185, .75); } + +.btn { + border-radius: 999px; + font-weight: 700; + letter-spacing: -0.01em; +} + +.btn-primary-neo { + color: #02111d; + background: linear-gradient(135deg, var(--primary), var(--secondary)); + border: 0; + box-shadow: 0 16px 36px rgba(16, 229, 185, 0.24); +} + +.btn-primary-neo:hover, .btn-primary-neo:focus { + color: #02111d; + transform: translateY(-1px); + box-shadow: 0 20px 44px rgba(40, 194, 255, 0.28); +} + +.btn-ghost, .btn-admin { + color: var(--text); + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.04); +} + +.btn-ghost:hover, .btn-admin:hover { border-color: var(--primary); background: rgba(16, 229, 185, 0.1); } + +.hero-section { padding: 4rem 0 2rem; } +.section-pad { padding: 5rem 0; } +.section-pad-sm { padding: 4rem 0 2rem; } + +.display-title { + font-size: clamp(3rem, 8vw, 6.6rem); + line-height: .92; + max-width: 12ch; +} + +.page-title { font-size: clamp(2.4rem, 5vw, 4.8rem); line-height: 1; max-width: 12ch; } +.hero-copy { color: #c7d8e8; font-size: clamp(1.05rem, 2vw, 1.25rem); max-width: 43rem; } +.eyebrow, .section-kicker { + display: inline-flex; + align-items: center; + gap: .5rem; + color: var(--primary); + text-transform: uppercase; + font-size: .78rem; + font-weight: 800; + letter-spacing: .14em; +} + +.eyebrow::before, .section-kicker::before { + content: ""; + width: .55rem; + height: .55rem; + border-radius: 50%; + background: var(--primary); + box-shadow: 0 0 24px rgba(16, 229, 185, .85); +} + +.text-muted-soft { color: var(--muted); } +.letter-spaced { letter-spacing: .16em; color: var(--muted); } + +.orb { + position: absolute; + border-radius: 999px; + filter: blur(6px); + opacity: .7; + pointer-events: none; +} +.orb-one { width: 18rem; height: 18rem; right: -4rem; top: 6rem; background: radial-gradient(circle, rgba(16,229,185,.28), transparent 62%); } +.orb-two { width: 12rem; height: 12rem; left: 45%; bottom: 1rem; background: radial-gradient(circle, rgba(255,176,32,.18), transparent 62%); } + +.glass-card, .surface-card, .metric-tile { + background: linear-gradient(145deg, rgba(16, 38, 61, .9), rgba(9, 24, 40, .82)); + border: 1px solid var(--line); + border-radius: var(--radius-xl); + box-shadow: var(--shadow); + backdrop-filter: blur(22px); +} + +.hero-panel { padding: clamp(1.5rem, 4vw, 2.5rem); position: relative; overflow: hidden; } +.hero-panel::after { + content: ""; + position: absolute; + inset: auto -4rem -5rem auto; + width: 14rem; + height: 14rem; + background: linear-gradient(135deg, rgba(16,229,185,.22), rgba(40,194,255,.16)); + transform: rotate(20deg); + border-radius: 2.5rem; +} +.status-dot { width: .85rem; height: .85rem; background: var(--primary); border-radius: 50%; box-shadow: 0 0 22px var(--primary); } + +.risk-meter { + width: 12.5rem; + height: 12.5rem; + border-radius: 50%; + display: grid; + place-items: center; + background: + radial-gradient(circle at center, #10263d 58%, transparent 59%), + conic-gradient(var(--primary), var(--secondary), var(--accent), var(--primary)); + position: relative; +} +.risk-meter span { display: block; font: 700 3.5rem/1 "Space Grotesk"; } +.risk-meter small { display: block; color: var(--muted); margin-top: .25rem; text-transform: uppercase; letter-spacing: .12em; font-weight: 800; font-size: .72rem; } +.risk-meter.detail { width: 15rem; height: 15rem; } +.risk-ring-warning { background: radial-gradient(circle at center, #10263d 58%, transparent 59%), conic-gradient(var(--accent), #ffd166, var(--accent)); } +.risk-ring-danger, .risk-ring-critical { background: radial-gradient(circle at center, #10263d 58%, transparent 59%), conic-gradient(var(--danger), var(--accent), var(--danger)); } + +.metric-card, .metric-tile { + padding: 1rem; + background: rgba(255,255,255,.045); + border: 1px solid var(--line); + border-radius: var(--radius-lg); +} +.metric-card strong, .metric-tile strong { display: block; font: 700 1.8rem/1 "Space Grotesk"; } +.metric-card span, .metric-tile span { color: var(--muted); font-size: .86rem; } +.metric-tile { padding: 1.4rem; } +.metric-tile strong { margin-top: .5rem; font-size: 2.2rem; } + +.trust-row { display: flex; flex-wrap: wrap; gap: .75rem; } +.trust-row span { padding: .5rem .8rem; border: 1px solid var(--line); border-radius: 999px; color: #d7e8f7; background: rgba(255,255,255,.04); } +.signal-list { display: grid; gap: .8rem; color: #d7e8f7; } +.signal { display: inline-block; width: .65rem; height: .65rem; border-radius: 50%; margin-right: .55rem; } +.signal.good { background: var(--success); } .signal.warn { background: var(--accent); } .signal.danger { background: var(--danger); } + +.scan-form textarea, .scan-form input[type="text"], .form-control { + width: 100%; + color: var(--text); + background: rgba(3, 16, 29, 0.72); + border: 1px solid var(--line); + border-radius: 18px; + padding: 1rem; +} +.scan-form textarea:focus, .form-control:focus { + color: var(--text); + background: rgba(3, 16, 29, .9); + border-color: var(--primary); + box-shadow: 0 0 0 .25rem rgba(16, 229, 185, .12); +} +.form-label { color: #dcecff; font-weight: 800; } +.form-text { color: var(--muted); } +.invalid-copy { color: #ff8ba0; font-weight: 700; margin-top: .5rem; } + +.scan-choice-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .75rem; } +.scan-choice { + display: flex; + align-items: center; + gap: .65rem; + padding: .9rem 1rem; + border: 1px solid var(--line); + border-radius: 18px; + background: rgba(255,255,255,.04); + cursor: pointer; + font-weight: 800; +} +.scan-choice:has(input:checked) { border-color: var(--primary); background: rgba(16,229,185,.12); } +.scan-choice input, .metadata-check input { accent-color: var(--primary); } + +.privacy-stack { display: grid; gap: 1rem; } +.privacy-stack article { padding: 1.2rem; border-radius: var(--radius-lg); border: 1px solid var(--line); background: rgba(255,255,255,.035); } +.privacy-stack span { color: var(--primary); font-weight: 900; } +.privacy-stack h3 { font-size: 1.15rem; margin: .4rem 0; } +.privacy-stack p { color: var(--muted); margin: 0; } + +.scan-card { + display: block; + min-height: 13rem; + padding: 1.35rem; + text-decoration: none; + border-radius: var(--radius-lg); + border: 1px solid var(--line); + background: linear-gradient(145deg, rgba(16,38,61,.86), rgba(9,24,40,.72)); + transition: transform .2s ease, border-color .2s ease; +} +.scan-card:hover { transform: translateY(-3px); border-color: var(--primary); color: var(--text); } +.scan-card strong { display: block; font: 700 2.2rem/1 "Space Grotesk"; margin: 1rem 0 .75rem; } +.scan-card p { color: #d6e7f6; } +.scan-card small { color: var(--muted); } + +.badge { border-radius: 999px; padding: .45rem .7rem; } +.risk-success { background: rgba(46,229,157,.15); color: #7dffc8; border: 1px solid rgba(46,229,157,.28); } +.risk-warning { background: rgba(255,176,32,.15); color: #ffd68a; border: 1px solid rgba(255,176,32,.3); } +.risk-danger, .risk-critical { background: rgba(255,77,109,.16); color: #ff9aad; border: 1px solid rgba(255,77,109,.34); } + +.empty-state { border: 1px dashed var(--line); border-radius: var(--radius-xl); background: rgba(255,255,255,.035); } +.empty-icon { font-size: 3rem; color: var(--primary); } + +.page-hero { background: linear-gradient(180deg, rgba(16,229,185,.07), transparent); border-bottom: 1px solid var(--line); } +.app-table { --bs-table-bg: transparent; --bs-table-border-color: var(--line); color: var(--text); } +.app-table th { color: var(--muted); text-transform: uppercase; font-size: .78rem; letter-spacing: .08em; } +.preview-cell { max-width: 24rem; color: #d7e8f7; } + +.scan-meta dt { color: var(--muted); text-transform: uppercase; letter-spacing: .08em; font-size: .75rem; margin-top: 1rem; } +.scan-meta dd { color: var(--text); margin-bottom: 0; word-break: break-word; } +.scan-meta code { color: var(--primary); } +.indicator-list { display: grid; gap: .8rem; } +.indicator-list article { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border-radius: 18px; + border: 1px solid var(--line); + background: rgba(255,255,255,.04); +} +.indicator-list p { color: var(--muted); margin: .25rem 0 0; } +.indicator-list span { color: var(--accent); font-weight: 900; } +.action-list { color: #d7e8f7; display: grid; gap: .7rem; } +.site-footer { color: var(--muted); border-top: 1px solid var(--line); background: rgba(3,16,29,.72); } + +@media (max-width: 767px) { + .hero-section { padding-top: 2.5rem; } + .scan-choice-grid { grid-template-columns: 1fr; } + .risk-meter { width: 10.5rem; height: 10.5rem; } + .display-title { max-width: 100%; } } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..1881a0c 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,287 @@ - +/* SentinelAI custom theme */ :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --bg: #06111f; + --bg-2: #0b1f33; + --surface: rgba(15, 34, 54, 0.82); + --surface-strong: #10263d; + --line: rgba(163, 205, 255, 0.16); + --text: #eef7ff; + --muted: #9fb6ca; + --primary: #10e5b9; + --primary-dark: #08a98a; + --secondary: #28c2ff; + --accent: #ffb020; + --danger: #ff4d6d; + --success: #2ee59d; + --shadow: 0 24px 80px rgba(0, 0, 0, 0.35); + --radius-xl: 28px; + --radius-lg: 20px; } + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + body { margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: + radial-gradient(circle at 10% 5%, rgba(16, 229, 185, 0.16), transparent 28rem), + radial-gradient(circle at 88% 20%, rgba(40, 194, 255, 0.14), transparent 24rem), + linear-gradient(135deg, var(--bg), #081827 54%, #03101d); + color: var(--text); min-height: 100vh; - text-align: center; - overflow: hidden; +} + +h1, h2, h3, .navbar-brand { + font-family: "Space Grotesk", "Inter", sans-serif; + letter-spacing: -0.04em; +} + +a { color: inherit; } + +a:hover { color: var(--primary); } + +.skip-link { + position: absolute; + left: -999px; + top: 0; + background: var(--primary); + color: #03101d; + padding: .75rem 1rem; + z-index: 9999; +} + +.skip-link:focus { left: 1rem; top: 1rem; } + +.app-nav { + background: rgba(6, 17, 31, 0.78); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(18px); +} + +.navbar-brand { color: var(--text); font-weight: 700; } +.navbar-brand:hover, .nav-link:hover { color: var(--primary); } +.nav-link { color: var(--muted); font-weight: 600; } +.navbar-toggler { border-color: var(--line); } +.navbar-toggler-icon { filter: invert(1); } +.brand-mark { color: var(--primary); text-shadow: 0 0 24px rgba(16, 229, 185, .75); } + +.btn { + border-radius: 999px; + font-weight: 700; + letter-spacing: -0.01em; +} + +.btn-primary-neo { + color: #02111d; + background: linear-gradient(135deg, var(--primary), var(--secondary)); + border: 0; + box-shadow: 0 16px 36px rgba(16, 229, 185, 0.24); +} + +.btn-primary-neo:hover, .btn-primary-neo:focus { + color: #02111d; + transform: translateY(-1px); + box-shadow: 0 20px 44px rgba(40, 194, 255, 0.28); +} + +.btn-ghost, .btn-admin { + color: var(--text); + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.04); +} + +.btn-ghost:hover, .btn-admin:hover { border-color: var(--primary); background: rgba(16, 229, 185, 0.1); } + +.hero-section { padding: 4rem 0 2rem; } +.section-pad { padding: 5rem 0; } +.section-pad-sm { padding: 4rem 0 2rem; } + +.display-title { + font-size: clamp(3rem, 8vw, 6.6rem); + line-height: .92; + max-width: 12ch; +} + +.page-title { font-size: clamp(2.4rem, 5vw, 4.8rem); line-height: 1; max-width: 12ch; } +.hero-copy { color: #c7d8e8; font-size: clamp(1.05rem, 2vw, 1.25rem); max-width: 43rem; } +.eyebrow, .section-kicker { + display: inline-flex; + align-items: center; + gap: .5rem; + color: var(--primary); + text-transform: uppercase; + font-size: .78rem; + font-weight: 800; + letter-spacing: .14em; +} + +.eyebrow::before, .section-kicker::before { + content: ""; + width: .55rem; + height: .55rem; + border-radius: 50%; + background: var(--primary); + box-shadow: 0 0 24px rgba(16, 229, 185, .85); +} + +.text-muted-soft { color: var(--muted); } +.letter-spaced { letter-spacing: .16em; color: var(--muted); } + +.orb { + position: absolute; + border-radius: 999px; + filter: blur(6px); + opacity: .7; + pointer-events: none; +} +.orb-one { width: 18rem; height: 18rem; right: -4rem; top: 6rem; background: radial-gradient(circle, rgba(16,229,185,.28), transparent 62%); } +.orb-two { width: 12rem; height: 12rem; left: 45%; bottom: 1rem; background: radial-gradient(circle, rgba(255,176,32,.18), transparent 62%); } + +.glass-card, .surface-card, .metric-tile { + background: linear-gradient(145deg, rgba(16, 38, 61, .9), rgba(9, 24, 40, .82)); + border: 1px solid var(--line); + border-radius: var(--radius-xl); + box-shadow: var(--shadow); + backdrop-filter: blur(22px); +} + +.hero-panel { padding: clamp(1.5rem, 4vw, 2.5rem); position: relative; overflow: hidden; } +.hero-panel::after { + content: ""; + position: absolute; + inset: auto -4rem -5rem auto; + width: 14rem; + height: 14rem; + background: linear-gradient(135deg, rgba(16,229,185,.22), rgba(40,194,255,.16)); + transform: rotate(20deg); + border-radius: 2.5rem; +} +.status-dot { width: .85rem; height: .85rem; background: var(--primary); border-radius: 50%; box-shadow: 0 0 22px var(--primary); } + +.risk-meter { + width: 12.5rem; + height: 12.5rem; + border-radius: 50%; + display: grid; + place-items: center; + background: + radial-gradient(circle at center, #10263d 58%, transparent 59%), + conic-gradient(var(--primary), var(--secondary), var(--accent), var(--primary)); position: relative; } +.risk-meter span { display: block; font: 700 3.5rem/1 "Space Grotesk"; } +.risk-meter small { display: block; color: var(--muted); margin-top: .25rem; text-transform: uppercase; letter-spacing: .12em; font-weight: 800; font-size: .72rem; } +.risk-meter.detail { width: 15rem; height: 15rem; } +.risk-ring-warning { background: radial-gradient(circle at center, #10263d 58%, transparent 59%), conic-gradient(var(--accent), #ffd166, var(--accent)); } +.risk-ring-danger, .risk-ring-critical { background: radial-gradient(circle at center, #10263d 58%, transparent 59%), conic-gradient(var(--danger), var(--accent), var(--danger)); } + +.metric-card, .metric-tile { + padding: 1rem; + background: rgba(255,255,255,.045); + border: 1px solid var(--line); + border-radius: var(--radius-lg); +} +.metric-card strong, .metric-tile strong { display: block; font: 700 1.8rem/1 "Space Grotesk"; } +.metric-card span, .metric-tile span { color: var(--muted); font-size: .86rem; } +.metric-tile { padding: 1.4rem; } +.metric-tile strong { margin-top: .5rem; font-size: 2.2rem; } + +.trust-row { display: flex; flex-wrap: wrap; gap: .75rem; } +.trust-row span { padding: .5rem .8rem; border: 1px solid var(--line); border-radius: 999px; color: #d7e8f7; background: rgba(255,255,255,.04); } +.signal-list { display: grid; gap: .8rem; color: #d7e8f7; } +.signal { display: inline-block; width: .65rem; height: .65rem; border-radius: 50%; margin-right: .55rem; } +.signal.good { background: var(--success); } .signal.warn { background: var(--accent); } .signal.danger { background: var(--danger); } + +.scan-form textarea, .scan-form input[type="text"], .form-control { + width: 100%; + color: var(--text); + background: rgba(3, 16, 29, 0.72); + border: 1px solid var(--line); + border-radius: 18px; + padding: 1rem; +} +.scan-form textarea:focus, .form-control:focus { + color: var(--text); + background: rgba(3, 16, 29, .9); + border-color: var(--primary); + box-shadow: 0 0 0 .25rem rgba(16, 229, 185, .12); +} +.form-label { color: #dcecff; font-weight: 800; } +.form-text { color: var(--muted); } +.invalid-copy { color: #ff8ba0; font-weight: 700; margin-top: .5rem; } + +.scan-choice-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .75rem; } +.scan-choice { + display: flex; + align-items: center; + gap: .65rem; + padding: .9rem 1rem; + border: 1px solid var(--line); + border-radius: 18px; + background: rgba(255,255,255,.04); + cursor: pointer; + font-weight: 800; +} +.scan-choice:has(input:checked) { border-color: var(--primary); background: rgba(16,229,185,.12); } +.scan-choice input, .metadata-check input { accent-color: var(--primary); } + +.privacy-stack { display: grid; gap: 1rem; } +.privacy-stack article { padding: 1.2rem; border-radius: var(--radius-lg); border: 1px solid var(--line); background: rgba(255,255,255,.035); } +.privacy-stack span { color: var(--primary); font-weight: 900; } +.privacy-stack h3 { font-size: 1.15rem; margin: .4rem 0; } +.privacy-stack p { color: var(--muted); margin: 0; } + +.scan-card { + display: block; + min-height: 13rem; + padding: 1.35rem; + text-decoration: none; + border-radius: var(--radius-lg); + border: 1px solid var(--line); + background: linear-gradient(145deg, rgba(16,38,61,.86), rgba(9,24,40,.72)); + transition: transform .2s ease, border-color .2s ease; +} +.scan-card:hover { transform: translateY(-3px); border-color: var(--primary); color: var(--text); } +.scan-card strong { display: block; font: 700 2.2rem/1 "Space Grotesk"; margin: 1rem 0 .75rem; } +.scan-card p { color: #d6e7f6; } +.scan-card small { color: var(--muted); } + +.badge { border-radius: 999px; padding: .45rem .7rem; } +.risk-success { background: rgba(46,229,157,.15); color: #7dffc8; border: 1px solid rgba(46,229,157,.28); } +.risk-warning { background: rgba(255,176,32,.15); color: #ffd68a; border: 1px solid rgba(255,176,32,.3); } +.risk-danger, .risk-critical { background: rgba(255,77,109,.16); color: #ff9aad; border: 1px solid rgba(255,77,109,.34); } + +.empty-state { border: 1px dashed var(--line); border-radius: var(--radius-xl); background: rgba(255,255,255,.035); } +.empty-icon { font-size: 3rem; color: var(--primary); } + +.page-hero { background: linear-gradient(180deg, rgba(16,229,185,.07), transparent); border-bottom: 1px solid var(--line); } +.app-table { --bs-table-bg: transparent; --bs-table-border-color: var(--line); color: var(--text); } +.app-table th { color: var(--muted); text-transform: uppercase; font-size: .78rem; letter-spacing: .08em; } +.preview-cell { max-width: 24rem; color: #d7e8f7; } + +.scan-meta dt { color: var(--muted); text-transform: uppercase; letter-spacing: .08em; font-size: .75rem; margin-top: 1rem; } +.scan-meta dd { color: var(--text); margin-bottom: 0; word-break: break-word; } +.scan-meta code { color: var(--primary); } +.indicator-list { display: grid; gap: .8rem; } +.indicator-list article { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + border-radius: 18px; + border: 1px solid var(--line); + background: rgba(255,255,255,.04); +} +.indicator-list p { color: var(--muted); margin: .25rem 0 0; } +.indicator-list span { color: var(--accent); font-weight: 900; } +.action-list { color: #d7e8f7; display: grid; gap: .7rem; } +.site-footer { color: var(--muted); border-top: 1px solid var(--line); background: rgba(3,16,29,.72); } + +@media (max-width: 767px) { + .hero-section { padding-top: 2.5rem; } + .scan-choice-grid { grid-template-columns: 1fr; } + .risk-meter { width: 10.5rem; height: 10.5rem; } + .display-title { max-width: 100%; } +}