From 097b3dbf095441c4876d2e314914fceca22472b3 Mon Sep 17 00:00:00 2001 From: pika Date: Sun, 30 Mar 2025 19:57:41 +0200 Subject: [PATCH] wip --- app/__init__.py | 7 +- app/__pycache__/__init__.cpython-313.pyc | Bin 8146 -> 8222 bytes app/core/__pycache__/auth.cpython-313.pyc | Bin 2469 -> 2737 bytes .../__pycache__/extensions.cpython-313.pyc | Bin 988 -> 928 bytes app/core/__pycache__/models.cpython-313.pyc | Bin 5710 -> 6478 bytes app/core/auth.py | 21 +- app/core/extensions.py | 28 +- app/core/models.py | 120 +++--- app/core/template_filters.py | 36 ++ app/routes/__pycache__/api.cpython-313.pyc | Bin 8371 -> 12302 bytes app/routes/__pycache__/auth.cpython-313.pyc | Bin 4112 -> 4387 bytes .../__pycache__/dashboard.cpython-313.pyc | Bin 13522 -> 15362 bytes app/routes/__pycache__/ipam.cpython-313.pyc | Bin 6468 -> 9043 bytes app/routes/api.py | 124 +++++- app/routes/auth.py | 54 +-- app/routes/dashboard.py | 113 +++-- app/routes/ipam.py | 162 ++++--- .../__pycache__/ip_scanner.cpython-313.pyc | Bin 7586 -> 7063 bytes app/scripts/db_seed.py | 54 ++- app/scripts/ip_scanner.py | 100 ++--- app/static/css/app.css | 182 ++++++-- app/static/js/app.js | 125 +++++- app/templates/dashboard/app_edit.html | 213 ++++++++++ app/templates/dashboard/app_form.html | 127 +++++- app/templates/dashboard/server_view.html | 395 +++++++++++++----- app/templates/errors/404.html | 6 +- app/templates/errors/500.html | 8 +- app/templates/ipam/subnet_view.html | 206 +++++---- app/templates/layout.html | 21 + config/__pycache__/settings.cpython-313.pyc | Bin 4014 -> 4243 bytes config/app-dev.db | Bin 0 -> 53248 bytes config/settings.py | 36 +- instance/development.db | Bin 36864 -> 36864 bytes run.py | 101 ++++- 34 files changed, 1719 insertions(+), 520 deletions(-) create mode 100644 app/core/template_filters.py create mode 100644 app/templates/dashboard/app_edit.html create mode 100644 config/app-dev.db diff --git a/app/__init__.py b/app/__init__.py index 104b799..9c3210c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -22,15 +22,16 @@ def create_app(config_name='development'): os.makedirs(os.path.join(app.instance_path), exist_ok=True) # Initialize extensions - from app.core.extensions import db, bcrypt, limiter, login_manager, csrf + from app.core.extensions import db, migrate, login_manager, bcrypt, limiter, csrf db.init_app(app) + migrate.init_app(app, db) + login_manager.init_app(app) bcrypt.init_app(app) - limiter.init_app(app) csrf.init_app(app) + limiter.init_app(app) # Initialize login manager from app.core.auth import User - login_manager.init_app(app) @login_manager.user_loader def load_user(user_id): diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc index 56b4d20b8dce93813f7dd41eb829a5679683f01e..0ea6980dc63903200ecbb4dca1a0b7c2be53aab3 100644 GIT binary patch delta 2430 zcma)7Urbw77(eI!Dcz<2OUu2tE4LIFR2VB1#+Vy_2Fpe}+S<#-WGt(NcB}?2^|p)A z7hEz!GLy-69-J>m6OEaB(eUDf59))PZi8+ij`YE&#i%h$jB!SN@tl^@E;8^weCIpg z`Tc(1_nklOgTeQPjK3R=9Khdqmwul8;i~Z??y??$v;0}%ta$Yc$tpAenUUEL6{J<@ z0TTlH%~W>{B3g(!(Pu_Gx&iq`F=$e$pjoAZ7L^HFRW@i-br>whT8Z@2--OT$OA!xH zsO=FGVlTCYp|{6WR2CIxH;AI!6~XR?!24hc)dd~%JT9D$b!@d2-|`e*5qc=uDuS2w zy_-?B1`}dmS?G$6ibCLHH^ZHYorMitj{7S)@ppY9J!$q#jTCuYbkOR@PGz#14CrWcmURa z%m~uv`AzV#Q)PZ-j710-4YAgvZ9$)E!^dT-92e0jV`XnuPTmXTCfO)A_fx7}vBwm2 zg_USAHi>>@_cbf_Dku(?B`RZVs!}7b^WmozN259)TZg-eBMp0wqW3=boJDU{=X2Lj z2+g1oOc}~TH6YOiaNBZC>6LP_o;*Jp-yHI2IhiX?@ueVBgKQvE37nwA9Vk=d>M~cg z=Tgij5TGiROF#yydDwgchVPcVbMQYb_YSPJYSvnUTX)dvsc4=2Ut(=Lh@CEpp$Pdm9p!td4h<-I5fQQjuQw#L395ZngY(wJ~x227&K1WQ6gI>2Bf{kd! zG6DzCpO$M__p9W6eBBe$T$V2P(A=BaAlw6(xf=%(!u&h*H3n1W+SayYT* zK}R2a3#kqXi(YZWT_Hk^5i?Fq7@Mt@^v>h(Z4`DkVC=l}ObPqq_Si?x{T@Wf4s6~4 zkIW5u$^Uunee>v-GkNn^UO%S!QCIzDLUJ9pNqDn9V0niukq1}YG@l_j3vqop&sY-e&ho=v5a+HnFfWK+3#CXvm)x}YVs=Shf8B$L_^ z^r_&6F0?ANN~9jmOH3&%@dCsQ5OWNfMb~HqyM>x`CNUqM&J=WsOd5~H6xvCGiV`zH z%xPlC|B{AZ2+%yl*s;mFkSdBE&(P42)`;hoW-=M=5)P0%kbQza7L%tZ;X_8*V025N zi=kzDovGVkY9BIn_nEq7`MP(Vac?lzhm3fi5w8c<8EJ#zmLeA;%OmRyzrpC2#xIU9 zH?1?yD%Lt9{GkJE(~9C6q2LsH*L?)OguZuwz1Ro7Y4MKtf?IlT*adF2dm|9scEf0Z zzAX(#jnuc&us_mD-7!-*bhoc>yqmt)1&QtD!aRL%&=j`Qt9BOqt1d1)K(Dr#!u|AW zKa2e}$VHmzwOUg|qSqvr_^n)&W7oP&QI=gBWTJgWDo+#XJj>#RJV)c4c_SC~v-x^c zw4Kej=*Vh2OCo-n_yO((3WcSBulUeXc;AW}sC5&s^hmK+&>xv!NM4vr4QNZ~6G_@L JNkMaQ`Cn39y2$_l delta 2288 zcma)6Uu;uV7(eI!X}fm)|L^v8-R)Y+R>tV&2Bk3Q$RLxJwxhSjz+|&q%b3~jn%f!S z#Ys#g@xg2yV+;={CPsRcX|I2XOVJ+eXS?ak8`nd|0C}!6; z0eCj04W8K)x5jK!PTZ1tt|61&^0XTOS!b5(p+qweUe#=Wl9p81*%EieoKjegt<^rT zmSVHjxt=zvA1lM9J5e(&v}!PW;b|oRqK#Ml8ZsQ}qitN))dNNQx+%1{JdEb(V3!Wh z!^N9o9;uO>PI}M3O|y;4z6?dtBigO=V!ghmM>lB!Mo}l@KHP$%#{YCN*1N2B5u21~ z^-ba8KI~$8;(de>T+uacv!cb54xo>WJo=4Mqb0`E>jguK(5ndR4>IFS$5zdhXF5?e zI?0-yt+6)Aj8Da^oC@(pbFAI$^a0T?>cveFO0r}vX%Ssvg@`MiQJhv;`lVH-JhK*M zCj6FX&As_X`4_9}a<3VIwFZWCw$LyfD} zYLM#!zzak*KJH$%2Y+10s(4B*VBV>5hYWRFFRW!AQV*)hee z=7oOL@@PlHT|Zl)@}m{i=$L%EI8#wiPE1YA6y%D2a(aA18kx#Txp5TM{)IQZTPH#P z<&SkK2sfkO8#@}wg;pZ>msj?pc6}Iz(3t+BYb%ZfDVf|Gayy1gKn@Y^MIJ*4Mo_{K zfT7DJgO-A==na#{)Jn2J0<{|SxoIc#q6emaxCgbF&tczh%$HyQ6)al=ZA75Cq_PCa?gc|g;JRTG61j17mE-YwRJazEpX56bbnS)xsLyr8 z!s9?iJ0=%$Glh}dVktl_|^u|jcX zVp^&+7G!x^KADs9lLa}*BZhC~Td+p1bt8=2&@#DE<$Wl|y9bh(R@4*4+|(Vk76NzL^yGu#TJyT{m;7~7n<%(zw<&Dq46#GHSbar~(Q ztbc_u-(&bChX0^@nGr5uboW#6DEisc3x|=f`MX&^xNi0(TEO)lUs4UeclweY;6?)+ z6zCheXevVepxf7(vQjs9Q#f?XA5JvWw>^+xi#Ey7x7!VgD1AH1V!oi(Cb!TFW N1)(8JL1Xpp`UkBTo$CMq diff --git a/app/core/__pycache__/auth.cpython-313.pyc b/app/core/__pycache__/auth.cpython-313.pyc index d9261975d28f1d1c17168e283c653e91baf85a37..5a7100f711642cbff89d99af13278f817e4699e7 100644 GIT binary patch delta 1442 zcmaJtc4QYTU zp0gy^8It!bNv@GNx$R! zpO=$KVlIMaxH3e;6|e)!YWcxXnVQ|3_A)?KtF2jhuYwMZE)}Mfp+Ckn<#4&UymMW z)3<~lF+FLmAkM7H((BgejrC#y5L?!vD#t8th77fMTjs8;qdvEQ_5z-1l9ap7I=_(zN`V>b=CUdFTjLDWvGm;v9 zrdrp?5i2`SnwcY-izzEZ+UTa#hCTEPsXfpFadMtP7lUs4o20F>@>N;H$1DbHGxnDN z@1jaDQkeYN?Oj*b)Z(R$!P4MPZ`u89;l+x-etl(aWwT*Ry`yelx~uKv_Ik_yk;3(g zuWo&8ZLB!EVcoX2d79ms-V2s}!-cDrVB^Nv?Xk_-E$fc8!{5yw1iNX*HLmKvOIV!2 z46oxq09&e46CeH7wJcYkQvm0h+!3lipgXgaN+;Q0POgH?D1BRwx!536dVt=O$2sDm z59N+kHjLwCyd=BvH=Voy;nN1yYVy$HQaYb8hsh))*q-c{03Prd<0EwO2#p<~*iT6N zCD20a3Q8r?Sx~-5&5wi-Zn-r8b1sw?9wBfZ^dQcEh{I(Ze#l|m2cxycGkmR&o}h=~*K*dmoG z*=$f$7L9XNrNE*=2r1d6tokEZ*eX@s5mm*~tO_Z6&Yi?9L0xdA`DV^{zVo_s?gxJw z@*H_QP6Vg1*{h7VJW;&gI(xnmM+6ba$7)ukY8>To0;RbK4%LX9(+oR?+YyvNM2R7y z=7gNG7~dtA4vC}?o5iRyAySL&@xq7W9OAc%ar6QZd=R!YYgzlgmqL?5m z5KP6KwCH5>LJ&)=i34sT5OG$ZN}m>`ML%ofU|32+s9i%K4kFKrRQsO37`D{^-1IFBWoHEIcZ=>XF(591T^Jio}1F`MAG#`6qrPw&pOX1=-ER)^^bknDkY zE#xS#6?B6p{}swzpGwg;`Kq6E(H#iimF_veb#C9>d%NwMXsZ);3iv6!&4pB$xs^2g zL98VDF7>&&zrSEO~0Nm)F7-_ua(Vw z{s7q(Xq4+*ec+2@{s8bb>T0gr(Oc2&cXsDmbNkaB?XBjeuG@P%b1So5+|_sVJ&{%S zmmXa1xGyy?cLSl_%#J^Ew|GzgPTv>5uO8MrfeVbP*~rDeWbF6=a~$~_VDh-Ki#<{| zRQsAG>SEWl$sljW(v0;&DPM;@rtgD2%C@wG!Y_0oreQ#~OR5!{O=@oGBKWAPAcrId$pP}>5Q06I$Jw>Cx`$w3!>FExiZF+u1kr&b+ mp4^&+DOX#oFAx~dr;y-#jAI=f`%}Pp%6B9pVc7n(%l!ixMHQz2 diff --git a/app/core/__pycache__/extensions.cpython-313.pyc b/app/core/__pycache__/extensions.cpython-313.pyc index b5466952e9ac18602534135b0c4e82895e20cf6c..30787d3831e25430a30e3a0ee9817a928dc6e48c 100644 GIT binary patch literal 928 zcmYjOzfaph6h1qNoy2hnfl?$YFbIJ%G=@qHNNuIks)GX+tyfEyu`iB=f27Z6Xd+cg zMK`9Z>fW_~N!Jb?!busbPHaW|13lXTE^fKyV;cX+J;g$x%uMXdy-3>Q0sCW4sZQm3p{pps#sQ`IDpGAwp# znhdgn3~mGMO>LlV6lU0*QJP^5y=*l6`DrN9D~vVoS(UMcJ7$P8d5eZT!yXuw4P;2V zq}Nnlm-l&Ca_gom2+(W?IT@P%*pa4=v@Jn~$y2xhQ$7`a|}WQCTCfpOu$hC!dC zL5E)8kxXp5n3#Zrh+xxl9KdnR0;HGvyVu9SM4Jq&RziAhKfP?YaqMF&#tLJK7b+xD zEZb%|3h8;V(q~R>TJ*whG?bp~>?nN?Wa2>kjT(iBC*F zttfuedPo~x--=Jo_`PrCT|t30O66P*^7f#ZYErIcr9t}ew3aI;S*IY(BxVK}Kuopf zG%+7TJ0YGA<@BJQADokJ+m4|Nl~m-N6Dm%Pshqi)K|WSm&FPtk&+m`eJlG)U(lj!s zrp;c(jbK29ygFJ;9jG`h><7AKUWo3iK}ghI6kye{h^4-Y>?FV;iB$)msg6fTq+0#H zYDa)o{DI&w_M$Lu_Xo86G>DwU$In4wH*xHXefIlT;5crAwkK$Pf;J~;Ed#p~v~?pO zp>bi1_^aytWosl}H6DGfkL2;n_DKGXmdC3H>Ao`Few^-`e=2jA%Ol}RY+igE6l-Ul*#EfRBUtA}&N<%E6 ztFFTQfh_Yg>OXLif)1TU=_)%#XiK~43>q+Z@!q}X+;g~`OZGdPo(h5=oci6lsn%(L zpYCv5%)FWYx=0t#6q1De9KnD>FHC~MEli@!Dq<~o>!R=rTxb;(ZxR$%_Q_n9lKnEj zLu0rqu7R?|g1QQT5ha=gIiN&D$WeJR^O0WN6$(2Ph7%x*7QhPzUR zLe4n@-HhL4mQ9<)Qh`cMZNDoWma)?^(Bm+cFoWhY^qMcI*b_H>i=Q*@&B(flI78kW zm5p*6E9i4U>gdp7W{&1ZDxUJVl*P^sEpG2aS!)@3vtbd<^0rWK80I≻>_nZouse z=NZ$45F64qBy3o-VatXGHmpw=U_$3gj~a2oFR>mo3dK#aCk*eWd%`uuM(oo0Y0ooe z0)!cf%foPL7<+hmI=Gh~MiUd(6J$Q^{~7zi7oGy+ta!LV;L(+OB@g^t&#pb&KkjHI nUOLEp-v2jdo!!6fm~&R2^B1Dfk1~a)bblpU$kF|r*XjQQ3D~!U diff --git a/app/core/__pycache__/models.cpython-313.pyc b/app/core/__pycache__/models.cpython-313.pyc index 24a2b32e793cf8e6162a97749c4990857bc0cdb4..e6b722aa26b465caff1dcd491968bf4e41476068 100644 GIT binary patch literal 6478 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 literal 5710 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 QcLwGT+&%D;Lza2^Pq50{u>b%7 diff --git a/app/core/auth.py b/app/core/auth.py index 4dd6cc5..8bcb78c 100644 --- a/app/core/auth.py +++ b/app/core/auth.py @@ -1,31 +1,34 @@ from flask_login import LoginManager, UserMixin from werkzeug.security import generate_password_hash, check_password_hash -from .extensions import db +from .extensions import db, bcrypt from datetime import datetime login_manager = LoginManager() login_manager.login_view = 'auth.login' class User(UserMixin, db.Model): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True, nullable=False) - password_hash = db.Column(db.String(128), nullable=False) + username = db.Column(db.String(64), unique=True, index=True) + email = db.Column(db.String(120), unique=True, index=True) + password_hash = db.Column(db.String(128)) is_admin = db.Column(db.Boolean, default=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow) + def __repr__(self): + return f'' + def set_password(self, password): - self.password_hash = generate_password_hash(password) - + self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8') + def check_password(self, password): - return check_password_hash(self.password_hash, password) + return bcrypt.check_password_hash(self.password_hash, password) def get_id(self): return str(self.id) - def __repr__(self): - return f'' - @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) \ No newline at end of file diff --git a/app/core/extensions.py b/app/core/extensions.py index fcf78a5..6a55299 100644 --- a/app/core/extensions.py +++ b/app/core/extensions.py @@ -1,28 +1,22 @@ from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager from flask_bcrypt import Bcrypt from flask_limiter import Limiter from flask_limiter.util import get_remote_address -from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect # Initialize extensions db = SQLAlchemy() -bcrypt = Bcrypt() +migrate = Migrate() login_manager = LoginManager() login_manager.login_view = 'auth.login' -csrf = CSRFProtect() +login_manager.login_message = 'Please log in to access this page.' +login_manager.login_message_category = 'info' -# Initialize rate limiter with fallback storage -try: - limiter = Limiter( - key_func=get_remote_address, - default_limits=["200 per day", "50 per hour"], - storage_uri="memory://" # Use memory storage for development - ) -except Exception as e: - print(f"Error initializing rate limiter: {e}") - # Fallback limiter with very basic functionality - limiter = Limiter( - key_func=get_remote_address, - default_limits=["200 per day", "50 per hour"] - ) +bcrypt = Bcrypt() +csrf = CSRFProtect() +limiter = Limiter( + key_func=get_remote_address, + default_limits=["200 per day", "50 per hour"] +) diff --git a/app/core/models.py b/app/core/models.py index 599b84f..34de4a4 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -1,77 +1,93 @@ -from .extensions import db +from app.core.extensions import db import json from datetime import datetime import ipaddress +from werkzeug.security import generate_password_hash, check_password_hash +from flask_login import UserMixin -class Subnet(db.Model): +# User model has been moved to app.core.auth +# Import it from there instead if needed: from app.core.auth import User + +class Port(db.Model): + __tablename__ = 'ports' + id = db.Column(db.Integer, primary_key=True) - cidr = db.Column(db.String(18), unique=True) # Format: "192.168.1.0/24" - location = db.Column(db.String(80)) - auto_scan = db.Column(db.Boolean, default=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + app_id = db.Column(db.Integer, db.ForeignKey('apps.id', ondelete='CASCADE'), nullable=False) + port_number = db.Column(db.Integer, nullable=False) + protocol = db.Column(db.String(10), default='TCP') # TCP, UDP, etc. + description = db.Column(db.String(200)) - @property - def network(self): - return ipaddress.ip_network(self.cidr) - - @property - def num_addresses(self): - return self.network.num_addresses - - @property - def used_ips(self): - # Count servers in this subnet - return Server.query.filter_by(subnet_id=self.id).count() + # Relationship + app = db.relationship('App', back_populates='ports') def __repr__(self): - return f'' - + return f'' class Server(db.Model): + __tablename__ = 'servers' + id = db.Column(db.Integer, primary_key=True) - hostname = db.Column(db.String(80), unique=True) - ip_address = db.Column(db.String(15), unique=True) - subnet_id = db.Column(db.Integer, db.ForeignKey('subnet.id')) - subnet = db.relationship('Subnet', backref=db.backref('servers', lazy=True)) + hostname = db.Column(db.String(64), nullable=False) + ip_address = db.Column(db.String(39), nullable=False) # IPv4 or IPv6 + subnet_id = db.Column(db.Integer, db.ForeignKey('subnets.id'), nullable=False) documentation = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - # Store ports as JSON in the database - _ports = db.Column(db.Text, default='[]') - - @property - def ports(self): - return json.loads(self._ports) if self._ports else [] - - @ports.setter - def ports(self, value): - self._ports = json.dumps(value) if value else '[]' - - def get_open_ports(self): - return self.ports + # Relationships + subnet = db.relationship('Subnet', back_populates='servers') + apps = db.relationship('App', back_populates='server', cascade='all, delete-orphan') def __repr__(self): return f'' - -class App(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(80)) - server_id = db.Column(db.Integer, db.ForeignKey('server.id')) - server = db.relationship('Server', backref=db.backref('apps', lazy=True)) - documentation = db.Column(db.Text) - created_at = db.Column(db.DateTime, default=datetime.utcnow) +class Subnet(db.Model): + __tablename__ = 'subnets' - # Store ports as JSON in the database - _ports = db.Column(db.Text, default='[]') + id = db.Column(db.Integer, primary_key=True) + cidr = db.Column(db.String(18), unique=True, nullable=False) # e.g., 192.168.1.0/24 + location = db.Column(db.String(64)) + active_hosts = db.Column(db.Text) # Store as JSON string + last_scanned = db.Column(db.DateTime) + auto_scan = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + servers = db.relationship('Server', back_populates='subnet') + + def __repr__(self): + return f'' @property - def ports(self): - return json.loads(self._ports) if self._ports else [] + def used_ips(self): + """Number of IPs used in this subnet (servers)""" + return len(self.servers) - @ports.setter - def ports(self, value): - self._ports = json.dumps(value) if value else '[]' + # Getter and setter for active_hosts as JSON + @property + def active_hosts_list(self): + if not self.active_hosts: + return [] + return json.loads(self.active_hosts) + + @active_hosts_list.setter + def active_hosts_list(self, hosts): + self.active_hosts = json.dumps(hosts) + +class App(db.Model): + __tablename__ = 'apps' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), nullable=False) + server_id = db.Column(db.Integer, db.ForeignKey('servers.id'), nullable=False) + documentation = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + server = db.relationship('Server', back_populates='apps') + ports = db.relationship('Port', back_populates='app', cascade='all, delete-orphan') def __repr__(self): return f'' \ No newline at end of file diff --git a/app/core/template_filters.py b/app/core/template_filters.py new file mode 100644 index 0000000..09ecdcc --- /dev/null +++ b/app/core/template_filters.py @@ -0,0 +1,36 @@ +import ipaddress +from flask import Blueprint + +bp = Blueprint('filters', __name__) + +@bp.app_template_filter('ip_network') +def ip_network_filter(cidr): + """Convert a CIDR string to an IP network object""" + try: + return ipaddress.ip_network(cidr, strict=False) + except ValueError: + return None + +@bp.app_template_filter('ip_address') +def ip_address_filter(ip): + """Convert an IP string to an IP address object""" + try: + return ipaddress.ip_address(ip) + except ValueError: + return None + +@bp.app_template_filter('markdown') +def markdown_filter(text): + """Convert markdown text to HTML""" + import markdown + if text: + return markdown.markdown(text, extensions=['tables', 'fenced_code']) + return "" + +@bp.app_template_global('get_ip_network') +def get_ip_network(cidr): + """Global function to get an IP network object from CIDR""" + try: + return ipaddress.ip_network(cidr, strict=False) + except ValueError: + return None \ No newline at end of file diff --git a/app/routes/__pycache__/api.cpython-313.pyc b/app/routes/__pycache__/api.cpython-313.pyc index d26b7f8b5beffdf6b954ab4690d39aba1da3eefe..ffd4419d5919118459c668b1b0ce05696d4d120b 100644 GIT binary patch literal 12302 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 delta 2142 zcmaJ>O>7%Q6rT02?Op#T_Qr|rB#xV=-8OAX=?`t%lq$4DZ4;_)a{vO18!yI!*zT;8 z5&=q%R7fphq(-#-^n%2J3o;TXF7(6&C8BDhg{p-EQpKfFD+GrM-Ww-tH!`fW-_Fka zX6C)`y*HO$s~!zK3bm^r3M1ThP*nKj1B1A)J zG9hG$?wJVdUR^#Q(#S;Q%qG^OL^mtCZ$Qxf2V4^#J&+)Kaa(xSzqHk11{;{6Cd{bA z4C@hR&z2_4n8R#s*fZLMsXEM915<6nY;%}x4a|5GX53-6H!wSzFx%Ie$;3KSPu6*~ z!?CBJ`slt{ts7FHu}#%Wssd zN-;ZS+P*^B$mVj?v@F_=3@>NkrtGHDA|%-FN;;DS3r!*5a=ICz4*{3ZP5@Y-4)dek zv==A45w-x>9*YM-yWq!i1ElexX|(zBX2r00uyi|2q}aUgXCYIa@Lv*yZuUoD-}V9I zd8fd^Y~Cz95(pX76e`0kO(1MV2q47RsbG4re)>}|X$z)>Zq<(rXUm1PBMnrN?gulz z)<^8sOsaY#xGKzXvD_P@XAq32{=B9%id&Oihorkk)a0>Hmq)AyZmZbOMEf4kXnhvR$G*iXa4x3 zHWG+_xbK4_OHzVnsvoPz)=IF}uaw8nqv9f9RGN=+y?767sJ;#K#=^s;wZeNAMo$)= zjLnFtp~Hl@$lJ__&p9@=w z`VJTbc&+aNABch=+$DQ{B75$TVBcLbuuKN-kjx#jg?+W-CVRXi$v)Znjc?V}BY63C F{{q)QvPJ*^ diff --git a/app/routes/__pycache__/auth.cpython-313.pyc b/app/routes/__pycache__/auth.cpython-313.pyc index 0922c44a9002b8142dbf129a3b4a1810ae54e562..08dccd58e03e5cd6022a80c1790f48351dbd3978 100644 GIT binary patch delta 1744 zcmbVN&rc&&9G{s^+iBmlLzx0C6&YG^sdOn~acQ@^n_XcSkr2b!6~n=d+p*Lwt-NUi zHW4%)WKRouiA#)85)*?54I3|eGVDKKW^9vgDlr~7=#34lSAB2lz#i5$@g2T--|zc; zzu)=(oK5Mgu=m*GaZ~vG`0?+#Ti~vDjA6cJ&>PMVX!rN`89VLlq^OK-E{N`OBKpygC0Ylb+X z^Am?$`)1-NZs^b-x-+vi)A08j{{EfoHGgcw^@|YLaW$lbAtmG6y0amkj!0tJ40ESQ@l8@ z^a+Op%`g^c*4Bq3GLP(#osZR4Jdtyxx@K+tmTayQE!#wzv`L2@BDhlV?fp;F9o9F`)3T6ciZHJko{-jPd(~%lDpYfMtqhfNzEp%=SRImt z!vH2tu7Z2Hs4S^4h}mk_H8)M+iw5Bvo$mnp%GAJw?-2 zN-zrrB64b~hJFLSOXhq=+0$<`(*a``q>=fR@PWfbg|kA1Nz4-8+PgHm>WJdOjjfB_ zchXzwh8Qu#$j)?4jBjv%I;ggeFQ&emGRJplcfQWQWRCB-dsRc6*7@l}Aly#fN;N>Q z0eW{-9rWrT)&Qdh7;S)r0TQNWYetZb0`DRjf2_fbj-M86bs@`LOTB4fX-Y zH@Kj|1-A$5Tm(&c241+dAFOk)K5|elaWhfp0(cjE?rbfFnvmAHw7C)DdsPEm(b+3- z1l{(md8)wLex*todA-t9)vZ*@i^JwV#3>~IfmS6>iuDZzM~Ku+paTQ3D!hOtGbDW^ zhaXapI8^fX(IaTf`)!o0L^Ho83Z@BrYJp2Hm9mwBdJ$elIbm7|&=-zv6m#~7!MCFX x+7~)g1N6%@5sn-&^&!nyFJjS@BjP0 z-}mvp3T=zd6P|Yv=-mF`kKFf;Z=919b)SOY+N+du04dS+Bu6l*m-mub@{45!&>B4`@q9@#NT zok}S6@VTCqIhjvVVM1~)z<=mDULt`XS(|hz`%r_^);CEH{M@?k?dd2LkW+yjv4BPd z`fc~vF~ne_`a1!KQMdu$;-m1$Hon5Twr@7Pk-9gs&osQ3>)y*Pu76wml^bsf!98x5 zYY3xtVRXOW5GJ;6ziMfbXsb8ybY2h77=yD$?=_vfcEs?yQ`&#+fVl!c zvR@&W;B)&1`5uDuJD9U%7FT_!hR%`gGV@Ze=fGkNWES_ja$)KLK`8wQx^5Kr-zeyz zd;2_Q&SgTjB}okY(h&T`GKl7bZVi$__@%`kmieg_ln>z|JD2H#+l*QbncPwsjj%Ts zG3D;RJ2Etv|M`;=PVYvBZkaCG4S#b67d_^r&42i%9qP6!U047E=r-WRM8E{+YP#Q7 zRP{j_WK!vJX)O4SbDQQh6ucnktF<$~MF|SG1E*M|}yYbnFsG)o*ml~_6SpcF)sp8^9d_WxgFrd>lvc8T) zT)uh>iu}7%fzaOc?sOv%s|R9we8vdO>WuK0jqnZaId&ZlAzl~a&n9&tt_#VAFk2U9 z4PkDJf58gyXWqxOZ3O2v*a);aL*wEWeaLVP##d*2JEI582+G5`pGVtEz*BpTGlVh2mC%{Q5$o3j=={aE-=*oxJxwfj4pj|80@Kc+^= k8zlCcXm{B59n$oZ4AOf@x{fIe8N(iKO!|&#Bz7+S7vSnnVE_OC diff --git a/app/routes/__pycache__/dashboard.cpython-313.pyc b/app/routes/__pycache__/dashboard.cpython-313.pyc index 0ec99618fcfa397144596138bef518f10088b7ef..59ff61cc152259832f7604afb0123388844f4458 100644 GIT binary patch delta 4528 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 delta 2672 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 literal 6468 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 diff --git a/app/routes/api.py b/app/routes/api.py index 4f2ff92..9fd0761 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -1,8 +1,10 @@ from flask import Blueprint, jsonify, request, abort from flask_login import login_required -from app.core.models import Subnet, Server, App +from app.core.models import Subnet, Server, App, Port from app.core.extensions import db from app.scripts.ip_scanner import scan +import random +import ipaddress bp = Blueprint('api', __name__, url_prefix='/api') @@ -93,14 +95,24 @@ def get_servers(): @bp.route('/servers/', methods=['GET']) @login_required def get_server(server_id): - """Get details for a specific server""" + """Get a specific server""" server = Server.query.get_or_404(server_id) apps = [] - for app in App.query.filter_by(server_id=server_id).all(): + for app in server.apps: + ports = [] + for port in app.ports: + ports.append({ + 'id': port.id, + 'port_number': port.port_number, + 'protocol': port.protocol, + 'description': port.description + }) + apps.append({ 'id': app.id, 'name': app.name, + 'ports': ports, 'created_at': app.created_at.strftime('%Y-%m-%d %H:%M:%S') }) @@ -110,9 +122,8 @@ def get_server(server_id): 'ip_address': server.ip_address, 'subnet_id': server.subnet_id, 'documentation': server.documentation, - 'created_at': server.created_at.strftime('%Y-%m-%d %H:%M:%S'), - 'ports': server.ports, - 'apps': apps + 'apps': apps, + 'created_at': server.created_at.strftime('%Y-%m-%d %H:%M:%S') } return jsonify(result) @@ -196,4 +207,103 @@ def suggest_ports(): return jsonify([ {'port': 80, 'type': 'tcp', 'desc': 'HTTP'}, {'port': 22, 'type': 'tcp', 'desc': 'SSH'} - ]) \ No newline at end of file + ]) + +@bp.route('/servers//suggest_port', methods=['GET']) +@login_required +def suggest_port(server_id): + """Suggest a random unused port for a server""" + server = Server.query.get_or_404(server_id) + + # Get all used ports for this server + used_ports = [] + for app in server.apps: + for port in app.ports: + used_ports.append(port.port_number) + + # Find an unused port in the dynamic/private port range + available_port = None + attempts = 0 + + while attempts < 50: # Try 50 times to find a random port + # Random port between 10000 and 65535 + port = random.randint(10000, 65535) + + if port not in used_ports: + available_port = port + break + + attempts += 1 + + if available_port is None: + # If no random port found, find first available in sequence + for port in range(10000, 65536): + if port not in used_ports: + available_port = port + break + + return jsonify({'port': available_port}) + +@bp.route('/apps//ports', methods=['GET']) +@login_required +def get_app_ports(app_id): + """Get all ports for an app""" + app = App.query.get_or_404(app_id) + + ports = [] + for port in app.ports: + ports.append({ + 'id': port.id, + 'port_number': port.port_number, + 'protocol': port.protocol, + 'description': port.description + }) + + return jsonify({'ports': ports}) + +@bp.route('/apps//ports', methods=['POST']) +@login_required +def add_app_port(app_id): + """Add a new port to an app""" + app = App.query.get_or_404(app_id) + + data = request.json + if not data or 'port_number' not in data: + return jsonify({'error': 'Missing port number'}), 400 + + port_number = data.get('port_number') + protocol = data.get('protocol', 'TCP') + description = data.get('description', '') + + # Check if port already exists for this app + existing_port = Port.query.filter_by(app_id=app_id, port_number=port_number).first() + if existing_port: + return jsonify({'error': 'Port already exists for this app'}), 400 + + new_port = Port( + app_id=app_id, + port_number=port_number, + protocol=protocol, + description=description + ) + + db.session.add(new_port) + db.session.commit() + + return jsonify({ + 'id': new_port.id, + 'port_number': new_port.port_number, + 'protocol': new_port.protocol, + 'description': new_port.description + }) + +@bp.route('/ports/', methods=['DELETE']) +@login_required +def delete_port(port_id): + """Delete a port""" + port = Port.query.get_or_404(port_id) + + db.session.delete(port) + db.session.commit() + + return jsonify({'success': True}) \ No newline at end of file diff --git a/app/routes/auth.py b/app/routes/auth.py index 1fe314c..2a41f8d 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -8,7 +8,7 @@ bp = Blueprint('auth', __name__, url_prefix='/auth') @bp.route('/login', methods=['GET', 'POST']) def login(): - # If already logged in, redirect to dashboard + """User login""" if current_user.is_authenticated: return redirect(url_for('dashboard.dashboard_home')) @@ -19,58 +19,64 @@ def login(): user = User.query.filter_by(email=email).first() - if user and user.check_password(password): - login_user(user, remember=remember) - next_page = request.args.get('next') - if next_page: - return redirect(next_page) - return redirect(url_for('dashboard.dashboard_home')) + if not user or not user.check_password(password): + flash('Invalid email or password', 'danger') + return render_template('auth/login.html', title='Login') - flash('Invalid email or password', 'danger') + login_user(user, remember=remember) + + next_page = request.args.get('next') + if not next_page or not next_page.startswith('/'): + next_page = url_for('dashboard.dashboard_home') + + return redirect(next_page) return render_template('auth/login.html', title='Login') @bp.route('/register', methods=['GET', 'POST']) def register(): - # If already logged in, redirect to dashboard + """User registration""" if current_user.is_authenticated: return redirect(url_for('dashboard.dashboard_home')) if request.method == 'POST': email = request.form.get('email') + username = request.form.get('username') password = request.form.get('password') - password_confirm = request.form.get('password_confirm') - # Check if email already exists - existing_user = User.query.filter_by(email=email).first() - if existing_user: - flash('Email already registered', 'danger') + # Validation + if not email or not username or not password: + flash('All fields are required', 'danger') return render_template('auth/register.html', title='Register') - # Check if passwords match - if password != password_confirm: - flash('Passwords do not match', 'danger') + if User.query.filter_by(email=email).first(): + flash('Email already registered', 'danger') + return render_template('auth/register.html', title='Register') + + if User.query.filter_by(username=username).first(): + flash('Username already taken', 'danger') return render_template('auth/register.html', title='Register') # Create new user - user = User(email=email) + user = User(email=email, username=username) user.set_password(password) - # Make first user an admin - if User.query.count() == 0: - user.is_admin = True - db.session.add(user) db.session.commit() - flash('Registration successful! Please log in.', 'success') - return redirect(url_for('auth.login')) + flash('Registration successful! You are now logged in.', 'success') + + # Auto-login after registration + login_user(user) + + return redirect(url_for('dashboard.dashboard_home')) return render_template('auth/register.html', title='Register') @bp.route('/logout') @login_required def logout(): + """User logout""" logout_user() flash('You have been logged out', 'info') return redirect(url_for('auth.login')) \ No newline at end of file diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 75539b5..ea731cb 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify from flask_login import login_required, current_user import markdown -from app.core.models import Server, App, Subnet +from app.core.models import Server, App, Subnet, Port from app.core.extensions import db, limiter from datetime import datetime @@ -196,22 +196,50 @@ def app_new(): server_id = request.form.get('server_id') documentation = request.form.get('documentation', '') + # Get port data from form + port_numbers = request.form.getlist('port_numbers[]') + protocols = request.form.getlist('protocols[]') + port_descriptions = request.form.getlist('port_descriptions[]') + # Basic validation if not name or not server_id: flash('Please fill in all required fields', 'danger') return render_template( 'dashboard/app_form.html', title='New Application', - servers=servers, - now=datetime.now() + servers=servers ) + # Create new app app = App( name=name, server_id=server_id, documentation=documentation ) + db.session.add(app) + db.session.flush() # Get the app ID without committing + + # Add ports if provided + for i in range(len(port_numbers)): + if port_numbers[i] and port_numbers[i].strip(): + try: + port_num = int(port_numbers[i]) + + # Get protocol and description, handling index errors + protocol = protocols[i] if i < len(protocols) else 'TCP' + description = port_descriptions[i] if i < len(port_descriptions) else '' + + new_port = Port( + app_id=app.id, + port_number=port_num, + protocol=protocol, + description=description + ) + db.session.add(new_port) + except (ValueError, IndexError): + continue + db.session.commit() flash('Application created successfully', 'success') @@ -220,8 +248,7 @@ def app_new(): return render_template( 'dashboard/app_form.html', title='New Application', - servers=servers, - now=datetime.now() + servers=servers ) @bp.route('/app/', methods=['GET']) @@ -245,49 +272,69 @@ def app_view(app_id): def app_edit(app_id): """Edit an existing application""" app = App.query.get_or_404(app_id) + servers = Server.query.all() if request.method == 'POST': name = request.form.get('name') server_id = request.form.get('server_id') documentation = request.form.get('documentation', '') - # Process ports - ports = [] - port_numbers = request.form.getlist('port[]') - port_types = request.form.getlist('port_type[]') - port_descs = request.form.getlist('port_desc[]') + # Get port data from form + port_numbers = request.form.getlist('port_numbers[]') + protocols = request.form.getlist('protocols[]') + port_descriptions = request.form.getlist('port_descriptions[]') - for i in range(len(port_numbers)): - if port_numbers[i]: - port = { - 'port': int(port_numbers[i]), - 'type': port_types[i] if i < len(port_types) else 'tcp', - 'desc': port_descs[i] if i < len(port_descs) else '', - 'status': 'open' - } - ports.append(port) + # Validate inputs + if not all([name, server_id]): + flash('All fields are required', 'danger') + return render_template('dashboard/app_form.html', + title='Edit Application', + app=app, + servers=servers, + edit_mode=True) # Update app app.name = name app.server_id = server_id app.documentation = documentation - app.ports = ports - db.session.commit() + # Delete existing ports and recreate them + # This simplifies handling additions, deletions, and updates + Port.query.filter_by(app_id=app.id).delete() - flash('Application updated successfully', 'success') - return redirect(url_for('dashboard.app_view', app_id=app.id)) + # Add new ports + for i in range(len(port_numbers)): + if port_numbers[i] and port_numbers[i].strip(): + try: + port_num = int(port_numbers[i]) + + # Get protocol and description, handling index errors + protocol = protocols[i] if i < len(protocols) else 'TCP' + description = port_descriptions[i] if i < len(port_descriptions) else '' + + new_port = Port( + app_id=app.id, + port_number=port_num, + protocol=protocol, + description=description + ) + db.session.add(new_port) + except (ValueError, IndexError): + continue + + try: + db.session.commit() + flash(f'Application {name} has been updated', 'success') + return redirect(url_for('dashboard.server_view', server_id=app.server_id)) + except Exception as e: + db.session.rollback() + flash(f'Error updating application: {str(e)}', 'danger') - # GET request - show form with current values - servers = Server.query.all() - - return render_template( - 'dashboard/app_edit.html', - title=f'Edit Application - {app.name}', - app=app, - servers=servers, - use_editor=True - ) + return render_template('dashboard/app_form.html', + title='Edit Application', + app=app, + servers=servers, + edit_mode=True) @bp.route('/app//delete', methods=['POST']) @login_required diff --git a/app/routes/ipam.py b/app/routes/ipam.py index 4039392..b6ad807 100644 --- a/app/routes/ipam.py +++ b/app/routes/ipam.py @@ -5,6 +5,7 @@ from app.core.extensions import db from app.scripts.ip_scanner import scan import ipaddress from datetime import datetime +import json bp = Blueprint('ipam', __name__, url_prefix='/ipam') @@ -16,7 +17,10 @@ def ipam_home(): # Calculate usage for each subnet for subnet in subnets: - subnet.usage_percent = subnet.used_ips / 254 * 100 if subnet.cidr.endswith('/24') else 0 + network = ipaddress.ip_network(subnet.cidr, strict=False) + max_hosts = network.num_addresses - 2 if network.prefixlen < 31 else network.num_addresses + used_count = Server.query.filter_by(subnet_id=subnet.id).count() + subnet.usage_percent = (used_count / max_hosts) * 100 if max_hosts > 0 else 0 return render_template( 'ipam/index.html', @@ -32,42 +36,43 @@ def subnet_new(): if request.method == 'POST': cidr = request.form.get('cidr') location = request.form.get('location') - auto_scan = 'auto_scan' in request.form + auto_scan = request.form.get('auto_scan') == 'on' # Basic validation if not cidr or not location: flash('Please fill in all required fields', 'danger') return render_template( 'ipam/subnet_form.html', - title='New Subnet', - now=datetime.now() + title='New Subnet' ) - # Check if valid CIDR + # Validate CIDR format try: - ipaddress.ip_network(cidr) + ipaddress.ip_network(cidr, strict=False) except ValueError: - flash('Invalid CIDR notation', 'danger') + flash('Invalid CIDR format', 'danger') return render_template( 'ipam/subnet_form.html', - title='New Subnet', - now=datetime.now() + title='New Subnet' ) - # Check if subnet already exists + # Check if CIDR already exists if Subnet.query.filter_by(cidr=cidr).first(): flash('Subnet already exists', 'danger') return render_template( 'ipam/subnet_form.html', - title='New Subnet', - now=datetime.now() + title='New Subnet' ) + # Create new subnet with JSON string for active_hosts, not a Python list subnet = Subnet( cidr=cidr, location=location, + active_hosts=json.dumps([]), # Convert empty list to JSON string + last_scanned=None, auto_scan=auto_scan ) + db.session.add(subnet) db.session.commit() @@ -76,70 +81,115 @@ def subnet_new(): return render_template( 'ipam/subnet_form.html', - title='New Subnet', - now=datetime.now() + title='New Subnet' ) @bp.route('/subnet/') @login_required def subnet_view(subnet_id): - """View subnet details""" + """View a specific subnet""" subnet = Subnet.query.get_or_404(subnet_id) + + # Get all servers in this subnet servers = Server.query.filter_by(subnet_id=subnet_id).all() - # Get network info - network = ipaddress.ip_network(subnet.cidr) - total_ips = network.num_addresses - 2 # Excluding network and broadcast addresses - used_ips = len(servers) - usage_percent = (used_ips / total_ips) * 100 if total_ips > 0 else 0 + # Parse CIDR for display + network = ipaddress.ip_network(subnet.cidr, strict=False) + subnet_info = { + 'network_address': str(network.network_address), + 'broadcast_address': str(network.broadcast_address), + 'netmask': str(network.netmask), + 'num_addresses': network.num_addresses, + 'host_range': f"{str(network.network_address + 1)} - {str(network.broadcast_address - 1)}" if network.prefixlen < 31 else subnet.cidr + } return render_template( 'ipam/subnet_view.html', - title=f'Subnet - {subnet.cidr}', + title=subnet.cidr, subnet=subnet, + subnet_info=subnet_info, servers=servers, - total_ips=total_ips, - used_ips=used_ips, - usage_percent=usage_percent, now=datetime.now() ) -@bp.route('/subnet//scan') +@bp.route('/subnet//edit', methods=['GET', 'POST']) +@login_required +def subnet_edit(subnet_id): + """Edit a subnet""" + subnet = Subnet.query.get_or_404(subnet_id) + + if request.method == 'POST': + cidr = request.form.get('cidr') + location = request.form.get('location') + auto_scan = request.form.get('auto_scan') == 'on' + + # Validate inputs + if not all([cidr, location]): + flash('All fields are required', 'danger') + return render_template('ipam/subnet_form.html', + title='Edit Subnet', + subnet=subnet, + edit_mode=True) + + # Validate CIDR format + try: + ipaddress.ip_network(cidr, strict=False) + except ValueError: + flash('Invalid CIDR format', 'danger') + return render_template('ipam/subnet_form.html', + title='Edit Subnet', + subnet=subnet, + edit_mode=True) + + # Update subnet + subnet.cidr = cidr + subnet.location = location + subnet.auto_scan = auto_scan + + try: + db.session.commit() + flash(f'Subnet {cidr} has been updated', 'success') + return redirect(url_for('ipam.subnet_view', subnet_id=subnet.id)) + except Exception as e: + db.session.rollback() + flash(f'Error updating subnet: {str(e)}', 'danger') + + return render_template('ipam/subnet_form.html', + title='Edit Subnet', + subnet=subnet, + edit_mode=True) + +@bp.route('/subnet//delete', methods=['POST']) +@login_required +def subnet_delete(subnet_id): + """Delete a subnet""" + subnet = Subnet.query.get_or_404(subnet_id) + + # Check if subnet has servers + servers_count = Server.query.filter_by(subnet_id=subnet_id).count() + if servers_count > 0: + flash(f'Cannot delete subnet {subnet.cidr}. It has {servers_count} servers assigned.', 'danger') + return redirect(url_for('ipam.subnet_view', subnet_id=subnet_id)) + + db.session.delete(subnet) + db.session.commit() + + flash(f'Subnet {subnet.cidr} has been deleted', 'success') + return redirect(url_for('ipam.ipam_home')) + +@bp.route('/subnet//scan', methods=['POST']) @login_required def subnet_scan(subnet_id): - """Scan a subnet for active hosts""" + """Manually scan a subnet""" subnet = Subnet.query.get_or_404(subnet_id) try: - results = scan(subnet.cidr, save_results=True) - flash(f'Scan completed for subnet {subnet.cidr}. Found {len(results)} active hosts.', 'success') + # Call the scan function with manual_trigger=True + scan(subnet, manual_trigger=True) + db.session.commit() + flash(f'Scan completed for subnet {subnet.cidr}', 'success') except Exception as e: - flash(f'Error scanning subnet: {e}', 'danger') + db.session.rollback() + flash(f'Error scanning subnet: {str(e)}', 'danger') - return redirect(url_for('ipam.subnet_view', subnet_id=subnet_id)) - -@bp.route('/subnet//visualize') -@login_required -def subnet_visualize(subnet_id): - """Visualize IP usage in a subnet""" - subnet = Subnet.query.get_or_404(subnet_id) - servers = Server.query.filter_by(subnet_id=subnet_id).all() - - # Create a dictionary of used IPs - used_ips = {server.ip_address: server.hostname for server in servers} - - # Get network info - network = ipaddress.ip_network(subnet.cidr) - total_ips = network.num_addresses - 2 # Excluding network and broadcast addresses - used_ip_count = len(servers) - - return render_template( - 'ipam/subnet_visualization.html', - title=f'Subnet Visualization - {subnet.cidr}', - subnet=subnet, - network=network, - used_ips=used_ips, - total_ips=total_ips, - used_ip_count=used_ip_count, - now=datetime.now() - ) \ No newline at end of file + return redirect(url_for('ipam.subnet_view', subnet_id=subnet_id)) \ No newline at end of file diff --git a/app/scripts/__pycache__/ip_scanner.cpython-313.pyc b/app/scripts/__pycache__/ip_scanner.cpython-313.pyc index 57cd5dd6b15d5c5bdfab8a18e0e4898e9c997db3..4977cae335167ebcb5947c77424e886e607a8b71 100644 GIT binary patch delta 2907 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 PVcH+P##As}mZJXv(JWW! diff --git a/app/scripts/db_seed.py b/app/scripts/db_seed.py index 8961144..6dcc278 100644 --- a/app/scripts/db_seed.py +++ b/app/scripts/db_seed.py @@ -1,23 +1,49 @@ from app.core.extensions import db -from app.core.models import Subnet, Server, App -from app.core.auth import User +from app.core.models import Subnet, Server, App, Port +from app.core.auth import User # Import User from auth module +import json def seed_database(): - # Example seed data for network objects - subnet = Subnet(cidr='192.168.1.0/24', location='Office', auto_scan=True) - server = Server(hostname='server1', ip_address='192.168.1.10', subnet=subnet) - app = App(name='Web App', server=server, documentation='# Welcome to Web App', - _ports='[{"port": 80, "type": "tcp", "desc": "Web"}]') - + """Add sample data to the database""" + # Create a default subnet if none exists + if Subnet.query.count() == 0: + subnet = Subnet( + cidr='192.168.1.0/24', + location='Office', + auto_scan=True, + active_hosts=json.dumps([]) + ) + db.session.add(subnet) + + # Create a sample server + server = Server( + hostname='server1', + ip_address='192.168.1.10', + subnet=subnet, + documentation='# Server 1\n\nThis is a sample server.' + ) + db.session.add(server) + + # Create a sample app + app = App( + name='Web App', + server=server, + documentation='# Welcome to Web App\n\nThis is a sample application.' + ) + db.session.add(app) + + # Add some ports + ports = [ + Port(app=app, port_number=80, protocol='TCP', description='HTTP'), + Port(app=app, port_number=443, protocol='TCP', description='HTTPS') + ] + db.session.add_all(ports) + # Create a default user if none exists if User.query.count() == 0: - admin = User(email="admin@example.com", is_admin=True) - admin.set_password("admin") + admin = User(username='admin', email='admin@example.com', is_admin=True) + admin.set_password('admin') db.session.add(admin) - - db.session.add(subnet) - db.session.add(server) - db.session.add(app) try: db.session.commit() diff --git a/app/scripts/ip_scanner.py b/app/scripts/ip_scanner.py index e54f5a5..b3f8cc2 100644 --- a/app/scripts/ip_scanner.py +++ b/app/scripts/ip_scanner.py @@ -7,63 +7,41 @@ from app.core.extensions import db from app.core.models import Subnet, Server import json import subprocess +import concurrent.futures +from datetime import datetime +import platform -def scan(cidr, max_threads=10, save_results=False): +def scan(subnet, manual_trigger=False): """ Scan a subnet for active hosts Args: - cidr: The subnet in CIDR notation (e.g. "192.168.1.0/24") - max_threads: Maximum number of threads to use - save_results: Whether to save results to the database - - Returns: - A list of dictionaries with IP, hostname, and status + subnet: The subnet object to scan + manual_trigger: If False, only scan if the subnet hasn't been scanned recently """ - print(f"Starting scan of {cidr}") - network = ipaddress.ip_network(cidr) + # Skip if not auto scan and not manually triggered + if not subnet.auto_scan and not manual_trigger: + return False - # Skip network and broadcast addresses for IPv4 - if network.version == 4: - hosts = list(network.hosts()) - else: - # For IPv6, just take the first 100 addresses to avoid scanning too many - hosts = list(network.hosts())[:100] + active_hosts = [] - # Split the hosts into chunks for multithreading - chunks = [[] for _ in range(max_threads)] - for i, host in enumerate(hosts): - chunks[i % max_threads].append(host) - - # Initialize results - results = [[] for _ in range(max_threads)] - - # Create and start threads - threads = [] - for i in range(max_threads): - if chunks[i]: # Only start a thread if there are IPs to scan - t = threading.Thread(target=scan_worker, args=(chunks[i], results, i)) - threads.append(t) - t.start() - - # Wait for all threads to complete - for t in threads: - t.join() - - # Combine results - all_results = [] - for r in results: - all_results.extend(r) - - # Save results to database if requested - if save_results: - try: - save_scan_results(cidr, all_results) - except Exception as e: - print(f"Error saving scan results: {e}") - - print(f"Scan completed. Found {len(all_results)} active hosts.") - return all_results + try: + # Parse the CIDR notation + network = ipaddress.ip_network(subnet.cidr, strict=False) + + # For each address in this network, ping it + for ip in network.hosts(): + if ping(str(ip)): + active_hosts.append(str(ip)) + + # Update subnet with scan results + subnet.active_hosts = json.dumps(active_hosts) + subnet.last_scanned = datetime.utcnow() + + return True + except Exception as e: + print(f"Error scanning subnet {subnet.cidr}: {str(e)}") + return False def scan_worker(ip_list, results, index): """Worker function for threading""" @@ -76,13 +54,23 @@ def scan_worker(ip_list, results, index): 'status': 'up' }) -def ping(ip): - """Ping an IP address and return True if it responds""" +def ping(host): + """ + Returns True if host responds to a ping request + """ + # Ping parameters based on OS + param = '-n' if platform.system().lower() == 'windows' else '-c' + # Build the command + command = ['ping', param, '1', '-w', '1', host] + try: - # Faster timeout (1 second) - subprocess.check_output(['ping', '-c', '1', '-W', '1', str(ip)], stderr=subprocess.STDOUT) - return True - except subprocess.CalledProcessError: + # Run the command and capture output + output = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=2) + # Return True if ping was successful + return output.returncode == 0 + except subprocess.TimeoutExpired: + return False + except Exception: return False def get_hostname(ip): @@ -166,7 +154,7 @@ def schedule_subnet_scans(): # Start a thread for each subnet thread = threading.Thread( target=scan, - args=(subnet.cidr,), + args=(subnet,), daemon=True ) thread.start() diff --git a/app/static/css/app.css b/app/static/css/app.css index 3f56c06..b81fa30 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -1,14 +1,35 @@ /* Custom styles for the app */ +:root { + --background-color: #f5f8fa; + --text-color: #333; + --card-bg: #fff; + --border-color: #e3e8ee; + --sidebar-bg: #f0f2f5; + --sidebar-hover-bg: #e0e5ee; + --highlight-color: #3b82f6; +} + +[data-bs-theme="dark"] { + --background-color: #1a2234; + --text-color: #e6e8eb; + --card-bg: #24304d; + --border-color: #374564; + --sidebar-bg: #151a27; + --sidebar-hover-bg: #1c2133; + --highlight-color: #3f8cff; +} + body { + background-color: var(--background-color); + color: var(--text-color); font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - background-color: #f5f7fb; - color: #232e3c; + transition: background-color 0.3s ease; } .markdown-body { padding: 1rem; - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.125); + background-color: var(--card-bg); + border: 1px solid var(--border-color); border-radius: 4px; } @@ -92,85 +113,160 @@ body { /* Sidebar styles */ .sidebar { - width: 260px; + background-color: var(--sidebar-bg); + color: var(--text-color); + height: 100vh; position: fixed; - top: 0; left: 0; - bottom: 0; - z-index: 100; - background: #fff; - box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15); + width: 250px; + z-index: 1000; overflow-y: auto; + transition: all 0.3s; } .sidebar-brand { - padding: 1.5rem; + padding: 1.5rem 1rem; + font-size: 1.25rem; + font-weight: 600; display: flex; align-items: center; - height: 64px; + color: var(--text-color); } .sidebar-brand-text { - font-size: 1.25rem; - font-weight: 600; - margin-left: 0.75rem; -} - -.sidebar-nav { - padding: 0.75rem 1.5rem; + margin-left: 0.5rem; } .sidebar-heading { + padding: 0.75rem 1rem 0.5rem; font-size: 0.75rem; - font-weight: 600; text-transform: uppercase; - color: #8898aa; - letter-spacing: 0.04em; - margin-top: 1.5rem; - margin-bottom: 0.75rem; + font-weight: 600; + color: var(--text-color); + opacity: 0.6; } .sidebar-item { display: block; - padding: 0.675rem 1.2rem; - font-size: 0.875rem; - color: #525f7f; - border-radius: 0.375rem; - margin-bottom: 0.25rem; - font-weight: 500; - transition: all 0.15s ease; + padding: 0.5rem 1rem; + color: var(--text-color); + text-decoration: none; + border-radius: 0.25rem; + margin: 0.2rem 0.5rem; + transition: background-color 0.2s; } .sidebar-item:hover { - color: #5e72e4; - background: rgba(94, 114, 228, 0.1); - text-decoration: none; + background-color: var(--sidebar-hover-bg); + color: var(--text-color); } .sidebar-item.active { - color: #5e72e4; - background: rgba(94, 114, 228, 0.1); + background-color: var(--highlight-color); + color: white; +} + +/* Main content */ +.main-content { + margin-left: 250px; + padding: 1rem; + min-height: 100vh; +} + +/* Header styles */ +.page-header { + margin-bottom: 1.5rem; +} + +.page-title { font-weight: 600; } -.main-content { - margin-left: 260px; +.page-pretitle { + color: #6c757d; + text-transform: uppercase; + font-size: 0.8rem; + letter-spacing: 0.05em; } -/* Responsive sidebar */ -@media (max-width: 992px) { +/* Auth pages */ +body.auth-page { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + background-color: var(--background-color); +} + +.auth-form { + max-width: 450px; + margin: 0 auto; +} + +/* Port visualization */ +.port-map { + overflow-x: auto; +} + +.port-map-grid { + display: grid; + grid-template-columns: repeat(10, 1fr); + gap: 4px; +} + +.port-item { + padding: 4px; + font-size: 10px; + text-align: center; + border-radius: 3px; + cursor: pointer; + user-select: none; +} + +.port-item:hover { + opacity: 0.8; +} + +/* Responsive tweaks */ +@media (max-width: 768px) { .sidebar { - left: -260px; - transition: left 0.3s ease; + transform: translateX(-100%); } .sidebar.show { - left: 0; + transform: translateX(0); } .main-content { margin-left: 0; } + + .main-content.sidebar-open { + margin-left: 250px; + } +} + +/* Theme switch */ +#theme-toggle { + width: 38px; + height: 38px; + position: relative; +} + +.theme-icon-light { + display: none; +} + +.theme-icon-dark { + display: inline-block; +} + +[data-bs-theme="dark"] .theme-icon-light { + display: inline-block; +} + +[data-bs-theme="dark"] .theme-icon-dark { + display: none; } /* Notification area */ diff --git a/app/static/js/app.js b/app/static/js/app.js index c349716..aca5674 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -58,6 +58,24 @@ document.addEventListener('DOMContentLoaded', () => { }); } }); + + // Wait for DOM to be fully loaded + document.addEventListener('DOMContentLoaded', function () { + // Initialize theme toggle + initThemeToggle(); + + // Initialize clipboard functionality + initClipboard(); + + // Initialize port map tooltips + initTooltips(); + + // Initialize mobile sidebar + initMobileSidebar(); + + // Initialize notifications + initNotifications(); + }); }); function initTiptapEditor(element) { @@ -134,9 +152,108 @@ function showNotification(message, type = 'info') { notificationArea.appendChild(notification); - // Remove notification after 3 seconds + // Auto-remove after 5 seconds setTimeout(() => { - notification.classList.remove('show'); - setTimeout(() => notification.remove(), 150); - }, 3000); + if (notification.parentNode) { + notification.remove(); + } + }, 5000); +} + +function initThemeToggle() { + const themeToggle = document.getElementById('theme-toggle'); + + if (themeToggle) { + themeToggle.addEventListener('click', function () { + const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + + document.documentElement.setAttribute('data-bs-theme', newTheme); + localStorage.setItem('theme', newTheme); + + console.log(`Theme switched to ${newTheme} mode`); + }); + } + + // Load saved theme or use OS preference + const storedTheme = localStorage.getItem('theme'); + if (storedTheme) { + document.documentElement.setAttribute('data-bs-theme', storedTheme); + } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + document.documentElement.setAttribute('data-bs-theme', 'dark'); + localStorage.setItem('theme', 'dark'); + } +} + +function initClipboard() { + // Add click handlers to any clipboard copy buttons + document.querySelectorAll('.copy-btn').forEach(btn => { + btn.addEventListener('click', function () { + const textToCopy = this.getAttribute('data-clipboard-text'); + if (textToCopy) { + navigator.clipboard.writeText(textToCopy) + .then(() => { + showNotification('Copied to clipboard!', 'success'); + }) + .catch(err => { + console.error('Failed to copy: ', err); + }); + } + }); + }); +} + +function initTooltips() { + const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + if (tooltips.length > 0) { + Array.from(tooltips).map(tooltipNode => new bootstrap.Tooltip(tooltipNode)); + } +} + +function initMobileSidebar() { + // Sidebar toggle for mobile + const sidebarToggler = document.querySelector('.sidebar-toggler'); + if (sidebarToggler) { + sidebarToggler.addEventListener('click', function () { + document.querySelector('.sidebar').classList.toggle('show'); + document.querySelector('.main-content').classList.toggle('sidebar-open'); + }); + } +} + +function initNotifications() { + // Add flash messages as notifications + const flashMessages = document.querySelectorAll('.alert.flash-message'); + flashMessages.forEach(message => { + setTimeout(() => { + const bsAlert = new bootstrap.Alert(message); + bsAlert.close(); + }, 5000); + }); +} + +// For random port suggestion +async function suggestRandomPort(serverId) { + try { + const response = await fetch(`/api/servers/${serverId}/suggest_port`); + if (!response.ok) throw new Error('Failed to get port suggestion'); + + const data = await response.json(); + if (data.port) { + // Copy to clipboard + navigator.clipboard.writeText(data.port.toString()) + .then(() => { + showNotification(`Port ${data.port} copied to clipboard!`, 'success'); + }) + .catch(err => { + console.error('Failed to copy: ', err); + showNotification(`Suggested free port: ${data.port}`, 'info'); + }); + } + return data.port; + } catch (error) { + console.error('Error:', error); + showNotification('Failed to suggest port', 'danger'); + return null; + } } diff --git a/app/templates/dashboard/app_edit.html b/app/templates/dashboard/app_edit.html new file mode 100644 index 0000000..c207dc3 --- /dev/null +++ b/app/templates/dashboard/app_edit.html @@ -0,0 +1,213 @@ +{% extends "layout.html" %} + +{% block content %} +
+ + +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+ +
+ + +
+
+ + +
+
+ + + Supports Markdown formatting +
+ + +
+ +
+ {% if app.ports %} + {% for port in app.ports %} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {% endfor %} + {% else %} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {% endif %} +
+
+ + +
+
+ +
+ + Cancel + + +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/dashboard/app_form.html b/app/templates/dashboard/app_form.html index 7b53bbf..d104542 100644 --- a/app/templates/dashboard/app_form.html +++ b/app/templates/dashboard/app_form.html @@ -46,12 +46,135 @@ placeholder="Use Markdown for formatting"> Supports Markdown formatting -
- Cancel + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+ +
+{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file diff --git a/app/templates/dashboard/server_view.html b/app/templates/dashboard/server_view.html index 9ef41e6..8182c55 100644 --- a/app/templates/dashboard/server_view.html +++ b/app/templates/dashboard/server_view.html @@ -5,133 +5,201 @@
-
-
-
-

Documentation

-
-
- {% if server.documentation %} - {{ markdown(server.documentation)|safe }} - {% else %} -
- No documentation available for this server. -
- {% endif %} -
-
- -
-
-
-

Applications

- - Add Application - -
-
-
- {% if apps %} - - - - - - - - - {% for app in apps %} - - - - - {% endfor %} - -
NamePorts
{{ app.name }} - {% for port in app.ports %} - - {{ port.port }}/{{ port.type }} {% if port.desc %}({{ port.desc }}){% endif %} - - {% endfor %} -
- {% else %} -
-
No applications registered for this server
- - Add Application - -
- {% endif %} -
-
-
-

Server Information

-
- Hostname: {{ server.hostname }} -
-
- IP Address: {{ server.ip_address }} -
-
- Subnet: {{ server.subnet.cidr if server.subnet else 'N/A' }} -
-
- Location: {{ server.subnet.location if server.subnet else 'N/A' }} -
-
- Created: {{ server.created_at.strftime('%Y-%m-%d') }} -
+
+
IP Address:
+
{{ server.ip_address }}
+ +
Subnet:
+
+ + {{ server.subnet.cidr }} + +
+ +
Location:
+
{{ server.subnet.location }}
+ +
Created:
+
{{ server.created_at.strftime('%Y-%m-%d') }}
+
+
-
-

Open Ports

+
+

Port Usage

+
+ +
- {% if server.get_open_ports() %} -
- {% for port in server.get_open_ports() %} -
-
-
- {{ port.port }} -
-
-
- {{ port.type|upper }} - {% if port.desc %} - {{ port.desc }} - {% endif %} +
+
+ {% for i in range(1, 101) %} + {% set port_num = 8000 + i - 1 %} + {% set port_used = false %} + {% set port_app = "" %} + {% set port_color = "" %} + {% set tooltip = "" %} + + {% for app in server.apps %} + {% for port in app.ports %} + {% if port.port_number == port_num %} + {% set port_used = true %} + {% set port_app = app.name %} + {% set port_color = "bg-" ~ ["primary", "success", "info", "warning", "danger"][(app.id % 5)] %} + {% set tooltip = app.name ~ " - " ~ port.description %} + {% endif %} + {% endfor %} + {% endfor %} + +
+ {{ port_num }} +
+ {% endfor %} +
+
+
+
+
Port Free
+
Port Used
+
+
+
+
+
+ +
+ +
+
+

Applications

+ +
+
+ {% if server.apps %} +
+ {% for app in server.apps %} +
+

+ +

+
+
+
+ + Edit + + +
+ + + {% if app.ports %} +
+
Ports
+
+ + + + + + + + + + {% for port in app.ports %} + + + + + + {% endfor %} + +
PortProtocolDescription
{{ port.port_number }}{{ port.protocol }}{{ port.description }}
+
+
+ {% endif %} + + + {% if app.documentation %} +
+
Documentation
+
+ {{ app.documentation|markdown }} +
+
+ {% else %} +
No documentation available
+ {% endif %}
{% endfor %}
{% else %} -
- No open ports detected. +
+
+ +
+

No applications found

+

+ This server doesn't have any applications yet. +

+
{% endif %}
@@ -139,4 +207,135 @@
+ + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block styles %} + {% endblock %} \ No newline at end of file diff --git a/app/templates/errors/404.html b/app/templates/errors/404.html index 77d505f..62832b5 100644 --- a/app/templates/errors/404.html +++ b/app/templates/errors/404.html @@ -2,13 +2,13 @@ {% block content %}
-
404
+
404

Page not found

- We are sorry but the page you are looking for was not found. + We are sorry but the page you are looking for was not found

- Return to dashboard + Go back to dashboard
{% endblock %} \ No newline at end of file diff --git a/app/templates/errors/500.html b/app/templates/errors/500.html index 9a72e56..762f998 100644 --- a/app/templates/errors/500.html +++ b/app/templates/errors/500.html @@ -2,13 +2,13 @@ {% block content %}
-
500
-

Internal Server Error

+
500
+

Server Error

- Something went wrong on our end. Please try again later. + Oops, something went wrong on our end

- Return to dashboard + Go back to dashboard
{% endblock %} \ No newline at end of file diff --git a/app/templates/ipam/subnet_view.html b/app/templates/ipam/subnet_view.html index 88a8663..84624d1 100644 --- a/app/templates/ipam/subnet_view.html +++ b/app/templates/ipam/subnet_view.html @@ -5,55 +5,129 @@ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} +
-
+
-

Registered Hosts

+

Subnet Information

- {% if servers %} + + + + + + + + + + {% set network = get_ip_network(subnet.cidr) %} + {% if network %} + + + + + + + + + + + + + + + + + + + + + {% endif %} + + + + + + + + + + + + +
CIDR Notation{{ subnet.cidr }}
Location{{ subnet.location }}
Network Address{{ network.network_address }}
Broadcast Address{{ network.broadcast_address if network.prefixlen < 31 else 'N/A' }}
Netmask{{ network.netmask }}
Host Range + {% if network.prefixlen < 31 %} {{ network.network_address + 1 }} - {{ network.broadcast_address - 1 }} + {% else %} {{ network.network_address }} - {{ network.broadcast_address }} {% endif %}
Total Hosts + {% if network.prefixlen < 31 %} {{ network.num_addresses - 2 }} {% else %} {{ network.num_addresses }} + {% endif %}
Auto Scan{{ 'Yes' if subnet.auto_scan else 'No' }}
Last Scanned{{ subnet.last_scanned|default('Never', true) }}
Created{{ subnet.created_at.strftime('%Y-%m-%d %H:%M') }}
+
+
+
+ +
+
+
+

Servers in Subnet

+
+
+ {% if subnet.servers %}
- +
- - + - {% for server in servers %} + {% for server in subnet.servers %} - + - @@ -62,69 +136,51 @@
Hostname IP AddressCreatedActions
{{ server.hostname }}{{ server.hostname }} + {{ server.ip_address }}{{ server.created_at.strftime('%Y-%m-%d') }} - View +
{% else %} -
-
No hosts registered in this subnet
- - Add New Server - - - Scan Subnet - +
+
+ +
+

No servers in this subnet

+

+ You can add a new server to this subnet from the dashboard +

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

Subnet Information

-
-
-
- Network: {{ subnet.cidr }} -
-
- Location: {{ subnet.location }} -
-
- Total IPs: {{ total_ips }} -
-
- Used IPs: {{ used_ips }} ({{ '%.1f'|format(usage_percent) }}%) -
-
- Available IPs: {{ total_ips - used_ips }} -
-
- Auto Scan: - {% if subnet.auto_scan %} - Enabled - {% else %} - Disabled - {% endif %} -
-
- Created: {{ subnet.created_at.strftime('%Y-%m-%d') }} -
-
+ + diff --git a/config/__pycache__/settings.cpython-313.pyc b/config/__pycache__/settings.cpython-313.pyc index d70e8e1e2c9fe4de1de5f848d7259409817c0111..a0c0577347e7fbe1da8ef0362fdc637c360afc99 100644 GIT binary patch delta 2214 zcmb7F&2JM|5Pxg0*WcexoDh>ZAyw)mwp&D6Z~`U4P6!1j_1Y+?fyk`a&W82w_U$@R zEL1}esOqIy?*N?ORG}VF=|9jz)vAXcf}%>sCwk=O$dM}bQ1v}KA0`M@9ch1WcHY~W z+2790J{v%Tj;7si0a(WGzAgaEbH@->J#MDHL7&lGXn|TBWzEpRT39P1rUpou=TbrYUMH8s&0QWKGTSqOcAl60aR> zlqBU=A;QVUV5Q^ini}Cmxs=O`S;)6`M@o{K>VF5(SATv9wK*?}&cj@tvXY#@l}P zgXZOmq^XgDEEhyRs|Yv&oYPxT^Ro2+-tRwxwqmMURui~ND`GCIWn+^;p;+jAx3)Dz zm%Eq37jOzZ&LX|3@ZkkT6J$v(JD0++6 z>5Fw2o4Alk&8M%eTue;I6DhWCYrj#E*H+~mbfG8uzJB5bdI&fPkQgmsu%fP^m-^Vb zT5o8J?rxaCiSb(BNG&kd(1GEr%#PX6!)(#Mh66C39UWz`H}zO+60Pl#IOD4BF0@Er zqXy9T^r^~(4%@JV@da(H5!zW3bQH`XOWhsk*LhJ^ikL;rNnR+t?!%|b-VS?^Yi&f* zbyqxbbtOKXo}QUz6Dyh2<-aZJ7owo?xT>RNOIA_BIetBoyE(f@loNUZcppF?iQGk? zn*d?9ZipvlGIOwp7#g}+ygJ^<29S>PNLQye7yE( z?XmPo+6sTN-GBZsKn+aROuqZ@!A5oC!R_kpCv#7ixBamfrdUG{41rgH!EL&?O&M{@ z;fqMM$3cCEr^eE|xijKPVuH>VfA%!5YJybQO~BMv|jYgcDQGTv(`zK$R zCfoe@1Gvm;9V`(q`z&D<0DTJqXexVCb{yShycHb)LBP_`v}|S#9Rt?FGSFHuhD>!A z?!;WF)rot&jbc}2&!Ksry1K2i+R9NyR<#1;ana$5E2%t;DOpwBkYVmT9>!U5RaS?! zsB++*!^s>7=`NCU^Ggc#nP)s1{SaXFEX^8N=8RsVGj#QiFOt!vNZ}n3);O^@8ur&9 z$j~V#8nkp#F0^48KtGzj=vUh3$j}`}DD>RyL$??ojr|^+8%~i9I3ld+@VNpAnb8&N z@$RxqlnZJU|0QxVr$+FNfcpu3Yvp^LNnVR%BcA?QsFjqEtvyIw$ZR`-F4^Lz40Syx zs?dQ0!6^c#G1P5&LF89fvr!s{F0K@L6ff{;k;(Iv8r` o{_HNm%dX2ujon}01$b#hfbYGUGf?vky>bS3tc*o>mua!|FU*8KQ2+n{ delta 2099 zcmaJ?O>7%Q6rQzr*X#YUD-&rb8Rq&#WAnSJx- z&Ae~sy`eonbh~PrCL<>2K6##XOFz1LIhlulCbiG#FsHwkP>ylanbD}bs3N+Ji`CpH9%!H%oN2kJ5 zu_O*-JUKm;j7*M)W0U8@rfQT^KS~(t@RvV<$dddXVb=c;iJ>s z5WOq6boU_SN0?6t(I#eSk!TBDlUn9wEMO5k_R*$bX%BT;OgGB8LUxDg})%^ z0AL@+7=ycrK67@=pFfDSpKevWku@o@0i3OSJ1cx^gE{c9pE;%gseRAjE0jQl!-&{0 zN9+kcT2HU~_zpz1oi22a)c>$81PR3CZEm2^L7B!eFgNs$I53}ZVjei($GDtOuwzeB zs_X=`tTP~+O*5pJxt1>HiiPb1=w^OR_5Y~XN;`8H`)S+uMxYTP5F0gJHt9TpXRp){pBElTzq1tu*L7xpZ`MGubYa``C`_H1vpdEKh7EDWbx`vN2*i`GIb82 zWxA>AZOUk=tl&_Tno<=Ek)1r37*GQoNTaOF^9AjWgbE&I(CPxhVr8YJqn)YHEBwl(N^|I*S>!vFYFs$@kO^89CeN2d<0r54As~qm2}8JT?bb-My6pLf9WL8G?wEbUtMmFP|W= z@&j1P$qCQ_z+EsRvkX~1_W2)Qd35D*;Zb4bql)j$3&eHL99UlFE8gifX}VrsHbf+{ zN2^unheyj(gC6nFIrmAgRKspo8=ygTL{`MmFKz^#^I-HOKs=+7p`w1)@+#ntP}e$xag8L diff --git a/config/app-dev.db b/config/app-dev.db new file mode 100644 index 0000000000000000000000000000000000000000..8cda1bcca817fa567ce75e65f9eb562ed2f52f56 GIT binary patch literal 53248 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 K_?YD%?*A76p=m(? delta 95 zcmZozz|^pSX@WE($3z)tMvjdMOZ+*Q_