├── LICENSE ├── installer.sh ├── portplow ├── api │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── client │ └── client.py ├── configs │ ├── etc │ │ ├── circus │ │ │ └── circus.ini │ │ ├── init │ │ │ └── portplow.conf │ │ └── nginx │ │ │ └── sites-available │ │ │ └── default │ ├── portplow.conf │ └── portplow.conf.default ├── manage.py ├── portplow │ ├── __init__.py │ ├── celery.py │ ├── middleware.py │ ├── settings.py │ ├── settings.py.default │ ├── urls.py │ └── wsgi.py ├── requirements.txt ├── scanner │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── basicauth.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── add_scanner.py │ │ │ ├── export_scan.py │ │ │ └── get_key.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20160421_1910.py │ │ ├── 0003_auto_20160421_1914.py │ │ ├── 0004_auto_20160423_0208.py │ │ ├── 0005_joblog_parsed.py │ │ ├── 0006_auto_20160505_1716.py │ │ ├── 0007_auto_20160513_0135.py │ │ └── __init__.py │ ├── models.py │ ├── signals.py │ ├── static │ │ ├── css │ │ │ ├── normalize.css │ │ │ ├── portplow.css │ │ │ └── sticky-footer-navbar.css │ │ ├── images │ │ │ └── minis_logo.png │ │ └── libs │ │ │ ├── bootstrap-daterangepicker-master.zip │ │ │ ├── bootstrap-daterangepicker │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── bower.json │ │ │ ├── daterangepicker.css │ │ │ ├── daterangepicker.js │ │ │ ├── daterangepicker.scss │ │ │ ├── demo.html │ │ │ ├── drp.png │ │ │ ├── moment.js │ │ │ ├── moment.min.js │ │ │ ├── package.js │ │ │ ├── package.json │ │ │ └── website │ │ │ │ ├── index.html │ │ │ │ ├── website.css │ │ │ │ └── website.js │ │ │ └── moment.js │ ├── tasks.py │ ├── templates │ │ └── scanner │ │ │ ├── base.html │ │ │ ├── base_no-header.html │ │ │ ├── base_plain.html │ │ │ ├── group-list.html │ │ │ ├── login.html │ │ │ ├── profile-create.html │ │ │ ├── profile-list.html │ │ │ ├── scan-create.html │ │ │ ├── scan-detail-all-jobs.html │ │ │ ├── scan-detail.html │ │ │ ├── scan-list.html │ │ │ ├── scanner-list.html │ │ │ ├── user-list.html │ │ │ └── user-logs.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── templates │ └── bootstrap.sh └── utils │ ├── __init__.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ └── __init__.py │ ├── templates │ └── utils │ │ ├── password_reset_email-utils.html │ │ ├── password_reset_form-utils.html │ │ └── user_add_form.html │ ├── tests.py │ ├── urls.py │ └── views.py └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Threat Express 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /installer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check if root 4 | if [[ $EUID -ne 0 ]]; then 5 | echo "This script must be run as root" 1>&2 6 | exit 1 7 | fi 8 | 9 | # Random password generator 10 | random_password() 11 | { 12 | cat /dev/urandom | tr -dc "a-zA-Z0-9\!@#\$^&*_+><~" | fold -w 32 | head -n 1 13 | } 14 | 15 | DEBIAN_FRONTEND=noninteractive 16 | 17 | read -p "What domain are you using? " DOMAIN 18 | read -p "What email address to use for SSL? " EMAIL 19 | read -p "Digital Ocean API token? " API_TOKEN 20 | read -p "Email username? " EMAIL_USER 21 | read -p "Email password? " EMAIL_PASS 22 | 23 | apt-get update 24 | apt-get dist-upgrade -y 25 | apt-get install -y ca-certificates wget git bc 26 | 27 | # Add nginx and postgres repositories 28 | echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list 29 | # echo "deb http://nginx.org/packages/mainline/ubuntu/ $(lsb_release -cs) nginx" > /etc/apt/sources.list.d/nginx.list 30 | add-apt-repository -y ppa:nginx/development 31 | 32 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - 33 | #wget --quiet -O - http://nginx.org/keys/nginx_signing.key | apt-key add - 34 | 35 | # Preseed packages 36 | #echo "postgresql-common postgresql-common/obsolete-major error" | /usr/bin/debconf-set-selections 37 | #echo "postgresql-common postgresql-common/catversion-bump note" | /usr/bin/debconf-set-selections 38 | echo "postgresql-common postgresql-common/ssl boolean true" | /usr/bin/debconf-set-selections 39 | echo "iptables-persistent iptables-persistent/autosave_v6 boolean true" | /usr/bin/debconf-set-selections 40 | echo "iptables-persistent iptables-persistent/autosave_v4 boolean true" | /usr/bin/debconf-set-selections 41 | 42 | # Install packages 43 | apt-get update 44 | apt-get install -y postgresql-9.5 nginx python3 python3-dev python3-pip redis-server libpq-dev 45 | 46 | pip3 install -r /opt/portplow/requirements.txt 47 | 48 | useradd -r portplow -d /opt/portplow 49 | 50 | mkdir -p /etc/circus 51 | ln -s /opt/portplow/configs/etc/circus/circus.ini /etc/circus/ 52 | 53 | cp /opt/portplow/configs/etc/init/portplow.conf /etc/init/ 54 | 55 | # Nginx configuration 56 | rm /etc/nginx/sites-enabled/default 57 | ln -s /opt/portplow/configs/etc/nginx/sites-available/default /etc/nginx/sites-enabled/portplow 58 | 59 | # Copy the config template over 60 | cp /opt/portplow/configs/portplow.conf.default ~portplow/.portplow.conf 61 | 62 | # Make sure the portplow user has access 63 | chown -R portplow:www-data /opt/portplow/ 64 | 65 | # Setup SSL encryption 66 | openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048 67 | git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt 68 | cd /opt/letsencrypt 69 | ./letsencrypt-auto certonly -a webroot --webroot-path=/var/www/html -d $DOMAIN --email $EMAIL --agree-tos 70 | 71 | SECRET_KEY=$(random_password) 72 | DB_PASS=$(random_password) 73 | PUB_IP=$(dig +short myip.opendns.com @resolver1.opendns.com) 74 | 75 | # Replace variables in the configuration file 76 | PORTPLOW_CONFIG=/opt/portplow/.portplow.conf 77 | sed -i "s/--api-token--/$API_TOKEN/" /opt/portplow/.portplow.conf 78 | sed -i "s/--public-ip--/$PUB_IP/g" /opt/portplow/.portplow.conf 79 | sed -i "s/--domain--/$DOMAIN/g" /opt/portplow/.portplow.conf 80 | sed -i "s/--api-token--/$TOKEN/g" /opt/portplow/.portplow.conf 81 | sed -i "s/--email-user--/$EMAIL_USER/g" /opt/portplow/.portplow.conf 82 | sed -i "s/--email-pass--/$EMAIL_PASS/g" /opt/portplow/.portplow.conf 83 | sed -i "s/--db-pass--/$DB_PASS/g" /opt/portplow/.portplow.conf 84 | sed -i "s/--secret-key--/$SECRET_KEY/g" /opt/portplow/.portplow.conf 85 | sed -i "s/--domain--/$DOMAIN/g" /opt/portplow/configs/etc/nginx/sites-available/default 86 | 87 | # Add database and user 88 | sudo -u postgres bash -c "psql -c \"CREATE USER portplow WITH PASSWORD '$DB_PASS';\"" 89 | sudo -u postgres createdb -O portplow portplow 90 | 91 | service nginx restart 92 | service postgresql restart 93 | service portplow restart 94 | 95 | cd /opt/portplow 96 | sudo -H -u portplow ./manage.py collectstatic --no-input 97 | sudo -H -u portplow ./manage.py migrate 98 | 99 | echo "Make sure you update the settings in ~portplow/.portplow.conf" 100 | echo "After making changes, restart the portplow service with 'service portplow restart'" 101 | -------------------------------------------------------------------------------- /portplow/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/api/__init__.py -------------------------------------------------------------------------------- /portplow/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = 'api' 6 | -------------------------------------------------------------------------------- /portplow/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/api/migrations/__init__.py -------------------------------------------------------------------------------- /portplow/api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.models import Group # User, 3 | from rest_framework import serializers 4 | from rest_framework.reverse import reverse 5 | 6 | from scanner.models import Scan, Scanner, Profile, Attachment, Job, JobLog 7 | 8 | User = get_user_model() 9 | 10 | 11 | class UserSerializer(serializers.ModelSerializer): 12 | 13 | full_name = serializers.CharField(source='get_full_name', read_only=True) 14 | links = serializers.SerializerMethodField() 15 | 16 | class Meta: 17 | model = User 18 | fields = ('id', User.USERNAME_FIELD, 'full_name', 19 | 'is_active', 'links', 'groups' ) 20 | 21 | def get_links(self, obj): 22 | request = self.context['request'] 23 | username = obj.get_username() 24 | return { 25 | 'self': reverse('user-detail', 26 | kwargs={User.USERNAME_FIELD: username}, request=request), 27 | # 'tasks': '{}?assigned={}'.format( 28 | # reverse('task-list', request=request), username) 29 | } 30 | 31 | 32 | class GroupSerializer(serializers.ModelSerializer): 33 | class Meta: 34 | model = Group 35 | fields = ('url', 'name') 36 | 37 | 38 | class QueueSerializer(serializers.ModelSerializer): 39 | status = serializers.SerializerMethodField() 40 | 41 | class Meta: 42 | model = Job 43 | 44 | def get_status(self, obj): 45 | return obj.get_status_display() 46 | 47 | 48 | class ScanSerializer(serializers.ModelSerializer): 49 | class Meta: 50 | model = Scan 51 | 52 | 53 | class ScannerSerializer(serializers.ModelSerializer): 54 | class Meta: 55 | model = Scanner 56 | 57 | 58 | class ProfileSerializer(serializers.ModelSerializer): 59 | 60 | # tool = serializers.SerializerMethodField() 61 | 62 | # def get_tool(self, obj): 63 | # return obj.get_tool_display() 64 | 65 | class Meta: 66 | model = Profile 67 | 68 | 69 | class ScannerBasicSerializer(serializers.ModelSerializer): 70 | class Meta: 71 | model = Scanner 72 | fields = ('ip', 'status') 73 | 74 | 75 | class JobBasicSerializer(serializers.ModelSerializer): 76 | status = serializers.SerializerMethodField() 77 | 78 | class Meta: 79 | model = Job 80 | fields = ('status', 'command', 'target_hosts') 81 | 82 | def get_status(self, obj): 83 | return obj.get_status_display() 84 | 85 | 86 | class JobLogSerializer(serializers.ModelSerializer): 87 | scanner = ScannerBasicSerializer(many=False, read_only=True) 88 | job = JobBasicSerializer(many=False, read_only=True) 89 | 90 | class Meta: 91 | model = JobLog 92 | fields = ('id', 'scanner', 'start_time', 'end_time', 'return_code', 93 | 'parsed', 'attempt', 'job') 94 | 95 | 96 | class AttachmentSerializer(serializers.ModelSerializer): 97 | class Meta: 98 | model = Attachment 99 | 100 | 101 | class DeconflictionSerializer(serializers.ListSerializer): 102 | 103 | pass 104 | -------------------------------------------------------------------------------- /portplow/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /portplow/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from rest_framework import routers 3 | from . import views 4 | 5 | router = routers.DefaultRouter() 6 | # router.register(r'users', views.UserViewSet) 7 | # router.register(r'groups', views.GroupViewSet) 8 | # router.register(r'scans', views.ScanViewSet) 9 | # router.register(r'scanners', views.ScannerViewSet) 10 | # router.register(r'profiles', views.ProfileViewSet) 11 | # router.register(r'reports', views.AttachmentViewSet) 12 | router.register(r'queue', views.QueueViewSet) 13 | router.register(r'results', views.JobLogViewSet) 14 | -------------------------------------------------------------------------------- /portplow/api/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from uuid import UUID 4 | 5 | # from json import JSONDecodeError 6 | 7 | from django.http import JsonResponse 8 | from django.contrib.auth import get_user_model 9 | from django.contrib.auth.models import Group 10 | from django.core.cache import cache 11 | from django.shortcuts import get_object_or_404 12 | 13 | from rest_framework.decorators import authentication_classes, api_view 14 | from rest_framework import viewsets, exceptions, authentication, permissions, filters 15 | from scanner.models import (Scan, 16 | Scanner, 17 | ScannerLog, 18 | Profile, 19 | Attachment, 20 | Job, 21 | JobLog, get_client_ip) 22 | from scanner.signals import outside_window, cleanup_scan 23 | from scanner.tasks import add_scanner_log, add_scanner_ip, assign_job 24 | 25 | # Internal libraries 26 | from api.serializers import (UserSerializer, 27 | GroupSerializer, 28 | ScanSerializer, 29 | ScannerSerializer, 30 | ProfileSerializer, 31 | QueueSerializer, 32 | JobLogSerializer, 33 | # LogSerializer, 34 | AttachmentSerializer) 35 | from portplow.settings import MAX_JOB_RETRIES 36 | 37 | 38 | def safe_get(lst, key): 39 | '''Get a value from a list or return None. 40 | ''' 41 | try: 42 | return lst['key'] 43 | except ValueError: 44 | return None 45 | 46 | 47 | User = get_user_model() 48 | 49 | 50 | class ScannerAuthentication(authentication.BaseAuthentication): 51 | 52 | auth_failed_msg = "Invalid token." 53 | 54 | def authenticate(self, request): 55 | auth = authentication.get_authorization_header(request).split() 56 | 57 | if not auth or auth[0].lower() != b'scannertoken': 58 | return None 59 | 60 | if len(auth) == 1: 61 | msg = 'Invalid token header. No credentials provided.' 62 | raise exceptions.AuthenticationFailed(msg) 63 | elif len(auth) > 2: 64 | msg = 'Invalid token header. Token string should not contain spaces.' 65 | raise exceptions.AuthenticationFailed(msg) 66 | 67 | try: 68 | token = auth[1].decode() 69 | except UnicodeError: 70 | raise exceptions.AuthenticationFailed(self.auth_failed_msg) 71 | 72 | try: 73 | scanner = Scanner.objects.get(token=token) 74 | except Scanner.DoesNotExist: 75 | raise exceptions.AuthenticationFailed(self.auth_failed_msg) 76 | 77 | return scanner, token 78 | 79 | def authenticate_header(self, request): 80 | return 'ScannerToken' 81 | 82 | 83 | class DefaultsMixin(object): 84 | """Default settings for view authentication, permissions, filtering and pagination.""" 85 | authentication_classes = ( 86 | authentication.BasicAuthentication, 87 | authentication.TokenAuthentication, 88 | authentication.SessionAuthentication, 89 | ) 90 | permission_classes = ( 91 | permissions.IsAuthenticated, 92 | ) 93 | paginate_by = 250 94 | paginate_by_param = "page_size" 95 | max_paginate_by = 250 96 | filter_backends = ( 97 | filters.DjangoFilterBackend, 98 | filters.SearchFilter, 99 | filters.OrderingFilter, 100 | ) 101 | 102 | 103 | class UserViewSet(DefaultsMixin, viewsets.ReadOnlyModelViewSet): 104 | """ 105 | API endpoint for listing users. 106 | """ 107 | lookup_field = User.USERNAME_FIELD 108 | lookup_url_kwarg = User.USERNAME_FIELD 109 | queryset = User.objects.order_by(User.USERNAME_FIELD) 110 | serializer_class = UserSerializer 111 | search_fields = (User.USERNAME_FIELD, ) 112 | 113 | 114 | class GroupViewSet(viewsets.ModelViewSet): 115 | """ 116 | API endpoint that allows groups to be viewed or edited. 117 | """ 118 | queryset = Group.objects.order_by('name') 119 | serializer_class = GroupSerializer 120 | 121 | 122 | class ScanViewSet(DefaultsMixin, viewsets.ModelViewSet): 123 | """ 124 | API endpoint that allows scans to be viewed or edited. 125 | """ 126 | queryset = Scan.objects.all() 127 | serializer_class = ScanSerializer 128 | 129 | 130 | class ScannerViewSet(DefaultsMixin, viewsets.ModelViewSet): 131 | """ 132 | API endpoint that allows scans to be viewed or edited. 133 | """ 134 | serializer_class = ScannerSerializer 135 | queryset = Scanner.objects.all() 136 | 137 | 138 | class ProfileViewSet(DefaultsMixin, viewsets.ModelViewSet): 139 | """ 140 | API endpoint that allows scans to be viewed or edited. 141 | """ 142 | queryset = Profile.objects.all() 143 | serializer_class = ProfileSerializer 144 | 145 | 146 | class QueueViewSet(DefaultsMixin, viewsets.ReadOnlyModelViewSet): 147 | """ 148 | API endpoint to view job status. 149 | """ 150 | def get_queryset(self): 151 | """ 152 | Restrict user to viewing only records they have access to... 153 | """ 154 | queryset = Job.objects.all() 155 | user = self.request.user 156 | scan_id = self.request.query_params.get('scan', False) 157 | if not scan_id and not user.is_superuser: 158 | raise exceptions.NotAcceptable 159 | 160 | if scan_id: 161 | queryset = queryset.filter(scan_id=scan_id) 162 | 163 | if self.request.user.is_superuser: 164 | return queryset 165 | 166 | queryset = queryset.filter(scan__group__in=user.groups.all()) 167 | return queryset 168 | 169 | queryset = Job.objects.all() 170 | serializer_class = QueueSerializer 171 | search_fields = ('target_hosts',) 172 | base_name = "queue" 173 | 174 | 175 | class JobLogViewSet(DefaultsMixin, viewsets.ReadOnlyModelViewSet): 176 | """ 177 | API endpoint to see results of jobs. 178 | """ 179 | 180 | def get_queryset(self): 181 | """ 182 | Restrict user to viewing only records they have access to... 183 | """ 184 | queryset = JobLog.objects.all().order_by('-start_time') 185 | user = self.request.user 186 | scan_id = self.request.query_params.get('scan', False) 187 | if not scan_id and not user.is_superuser: 188 | raise exceptions.NotAcceptable 189 | 190 | if scan_id: 191 | queryset = queryset.filter(job__scan_id=scan_id) 192 | 193 | if self.request.user.is_superuser: 194 | return queryset 195 | 196 | queryset = queryset.filter(job__scan__group__in=user.groups.all()) 197 | return queryset 198 | 199 | queryset = JobLog.objects.all() 200 | serializer_class = JobLogSerializer 201 | 202 | 203 | # class LogViewSet(DefaultsMixin, viewsets.ModelViewSet): 204 | # """ 205 | # API endpoint that allows logs to be viewed or added. 206 | # """ 207 | # queryset = Log.objects.order_by('-when') 208 | # serializer_class = LogSerializer 209 | # 210 | # def partial_update(self, request, *args, **kwargs): 211 | # raise exceptions.MethodNotAllowed 212 | # 213 | # def destroy(self, request, *args, **kwargs): 214 | # raise exceptions.MethodNotAllowed 215 | # 216 | # def update(self, request, *args, **kwargs): 217 | # raise exceptions.MethodNotAllowed 218 | 219 | 220 | class AttachmentViewSet(DefaultsMixin, viewsets.ModelViewSet): 221 | """ 222 | API endpoint that allows reports to be viewed or added. 223 | """ 224 | queryset = Attachment.objects.all() 225 | serializer_class = AttachmentSerializer 226 | 227 | def partial_update(self, request, *args, **kwargs): 228 | raise exceptions.MethodNotAllowed 229 | 230 | def destroy(self, request, *args, **kwargs): 231 | raise exceptions.MethodNotAllowed 232 | 233 | def update(self, request, *args, **kwargs): 234 | raise exceptions.MethodNotAllowed 235 | 236 | 237 | @api_view(['POST']) 238 | @authentication_classes((ScannerAuthentication,)) 239 | def checkin(request, id=None): 240 | """ 241 | Allow clients to check-in to the server to obtain jobs and return results. 242 | """ 243 | log = logging.getLogger(__name__) 244 | 245 | if id is None: 246 | raise exceptions.PermissionDenied 247 | # log.debug("Found our target function. {}".format(id)) 248 | # scanner = request.scanner 249 | # print("CHECKIN: {} -- {}".format(scanner.id, scanner.ip)) 250 | scanner = get_object_or_404(Scanner, id=UUID(id)) 251 | remote_ip = get_client_ip(request) 252 | if scanner.ip != remote_ip: 253 | add_scanner_ip.delay(scanner_id=str(scanner.id), new_ip=remote_ip) 254 | # log.debug("Received connection from: {}".format(remote_ip)) 255 | # scanner.ip = remote_ip 256 | # scanner.save() 257 | 258 | # Store any extra data as a log entry. 259 | log_entry = "{}, {}".format(scanner.ip, request.body) 260 | add_scanner_log.delay(scanner_id=str(scanner.id), 261 | log_type=ScannerLog.CHECKIN, 262 | scanner_ip=remote_ip, 263 | content=log_entry) 264 | 265 | content = json.loads(request.body.decode('utf8')) 266 | scanner_cache_key = "__scanner.ex.{}".format(str(scanner.id)) 267 | 268 | # Update job information if any is passed. 269 | if 'jobs' in content: 270 | # log.debug("Has job section.") 271 | if len(content['jobs']) > 0: 272 | log.debug("Has at least one job.") 273 | for job_update in content['jobs']: 274 | log.debug("checking job update") 275 | 276 | id = job_update.get('id') 277 | if id is None: 278 | log.error("No ID given for job specified in posted JSON.") 279 | continue 280 | 281 | # Check that job actually exists. 282 | job = cache.get(scanner_cache_key) 283 | if job is None: 284 | log.debug("Scanner is not assigned any job.") 285 | raise exceptions.PermissionDenied("Permission denied to selected scanner.") 286 | elif str(job.id) != id: 287 | log.debug("Scanner is not assigned this job -- {}.".format(id)) 288 | raise exceptions.PermissionDenied("Permission denied to selected scanner.") 289 | 290 | stderr = job_update.get('stderr') 291 | stdout = job_update.get('stdout') 292 | status = job_update.get('status') 293 | return_code = job_update.get('return_code') 294 | attempt = job_update.get('attempt', 0) 295 | 296 | print("status: {}, return_code: {}, attempt: {}".format(status, return_code, attempt)) 297 | 298 | record = JobLog.objects.filter(scanner=scanner, job=job, attempt=attempt) 299 | if record.count() > 0: 300 | job_record = record.first() 301 | else: 302 | job_record = JobLog(scanner=scanner, job=job, attempt=attempt) 303 | 304 | if stderr is not None: 305 | job_record.stderr += stderr 306 | 307 | if stdout is not None: 308 | job_record.stdout += stdout 309 | 310 | job_record.return_code = return_code 311 | job_record.status = status 312 | job_record.save() 313 | 314 | # Update the job record accordingly. 315 | if status == Job.COMPLETE: 316 | log.debug("Reported status as complete.") 317 | job.status = Job.COMPLETE 318 | job.scanner_lock = None 319 | cache.delete(scanner_cache_key) 320 | job_key = "__scanner.job.{}".format(str(job.id)) 321 | cache.delete(job_key) 322 | job.attempts += 1 323 | elif status == Job.ERROR: 324 | log.debug("Reported status as Error.") 325 | if attempt >= MAX_JOB_RETRIES: 326 | job.status = Job.KILLED 327 | else: 328 | job.status = Job.RETRY 329 | 330 | job.scanner_lock = None 331 | cache.delete(scanner_cache_key) 332 | # add_or_update_job_record(scanner_id=scanner.id, job_id=str(job.id), attempt=attempt, ) 333 | job.save() 334 | 335 | if outside_window(scanner.scan.id): 336 | # Remove all running jobs 337 | for job in scanner.executing_jobs.all(): 338 | job_key = "__scanner.job.{}".format(str(job.id)) 339 | cache.delete(job_key) 340 | job.status = Job.RETRY 341 | job.scanner_lock = None 342 | job.save() 343 | log.debug("Killed job {} on scanner at IP {} because it was outside the window.".format(job.id, scanner.ip)) 344 | print("Killed job {} on scanner at IP {} because it was outside the window.".format(job.id, scanner.ip)) 345 | return JsonResponse({"message": "kill_all"}) 346 | 347 | if content['status'] == "ready": 348 | next_job = scanner.scan.get_next_job(scanner=scanner) 349 | # log.debug("Gave us a status as ready.") 350 | # log.debug("Scanner: {}".format(scanner)) 351 | if next_job: 352 | # log.debug("We have a job.") 353 | content = "Assigned job `{}`".format(next_job.command) 354 | assign_job.delay(str(next_job.id), str(scanner.id)) 355 | add_scanner_log.delay(scanner_id=str(scanner.id), 356 | log_type=ScannerLog.ASSIGNED, 357 | scanner_ip=remote_ip, 358 | content=content) 359 | cache.set(scanner_cache_key, next_job, timeout=None) 360 | resp = JsonResponse({ 361 | "jobs": [ 362 | { 363 | "id": next_job.id, 364 | "command": next_job.command, 365 | "tool": next_job.profile.tool, 366 | "attempt": next_job.attempts, 367 | }, 368 | ] 369 | }) 370 | # log.debug("returning. {}".format(resp)) 371 | return resp 372 | 373 | return JsonResponse({"message": "ok"}) 374 | -------------------------------------------------------------------------------- /portplow/client/client.py: -------------------------------------------------------------------------------- 1 | ''' 2 | license... 3 | ''' 4 | 5 | # Standard libraries 6 | import json 7 | import logging 8 | import os 9 | import requests 10 | import sys 11 | import shlex 12 | import tempfile 13 | from datetime import datetime 14 | from distutils.dir_util import mkpath, DistutilsFileError 15 | from subprocess import Popen, PIPE, STDOUT 16 | from time import sleep 17 | 18 | ''' 19 | Setup logging to stdout for informational messages and console for 20 | everything else. 21 | ''' 22 | logging.basicConfig(level=logging.DEBUG, 23 | format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', 24 | datefmt='%m-%d %H:%M', 25 | filename='/var/opt/portplow/client.log', 26 | filemode='a+') 27 | 28 | # Attempt to use the coloredlogs library if available 29 | try: 30 | import coloredlogs 31 | formatter = coloredlogs.ColoredFormatter(fmt='%(name)-12s: %(levelname)-8s %(message)s') 32 | except ImportError: 33 | logging.getLogger('').warn("Colorized logs disabled. Install coloredlogs to use.") 34 | formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') 35 | 36 | console = logging.StreamHandler() 37 | console.setLevel(logging.INFO) 38 | console.setFormatter(formatter) 39 | logging.getLogger('').addHandler(console) 40 | 41 | VALID_TOOLS = [ 42 | "/usr/bin/nmap", 43 | "/opt/portplow/masscan/bin/masscan" 44 | ] 45 | 46 | 47 | class Job(object): 48 | 49 | log = logging.getLogger('job') 50 | 51 | def __init__(self, **kwargs): 52 | self.id = kwargs.get('id', None) 53 | self.status = kwargs.get('status', None) 54 | self.command = kwargs.get('command', None) 55 | self.return_code = kwargs.get('return_code', None) 56 | self.stdout = kwargs.get('stdout', None) 57 | self.stderr = kwargs.get('stderr', None) 58 | self.attempt = kwargs.get('attempt', 0) 59 | self.files = kwargs.get('files', None) 60 | self.tool = kwargs.get('tool', None) 61 | 62 | timestamp = datetime.utcnow().strftime("%Y-%m-%d_%H%M%S") 63 | self.output_dir = os.path.join(os.environ.get('PORTPLOW_DIR', '/tmp'), 64 | "results", 65 | str(self.id), 66 | timestamp) 67 | try: 68 | os.makedirs(self.output_dir, exist_ok=True) 69 | except TypeError: 70 | try: 71 | mkpath(self.output_dir) 72 | except DistutilsFileError: 73 | self.log.error("Cannot create output directory.") 74 | 75 | # Tack on the path to the command 76 | if self.tool in VALID_TOOLS: 77 | self.command = "{} {}".format(self.tool, self.command) 78 | else: 79 | self.log.error("An invalid tool was specified!") 80 | self.command = "/bin/false" 81 | 82 | self.log.debug("Job created: {}".format(kwargs)) 83 | 84 | def json_ready(self): 85 | # TODO: grab files if the job is complete. 86 | return { 87 | "id": self.id, 88 | "attempt": self.attempt, 89 | "status": self.status, 90 | "return_code": self.return_code, 91 | "stdout": self.stdout, 92 | "stderr": self.stderr, 93 | } 94 | 95 | 96 | class JobMonitor: 97 | """Job monitor class 98 | """ 99 | 100 | # Client constants 101 | READY = "ready" 102 | IN_USE = "in_use" 103 | 104 | # Job constants 105 | PENDING = 'P' 106 | EXECUTING = 'E' 107 | RETRY = 'R' 108 | KILLED = 'K' 109 | COMPLETE = 'C' 110 | HOLD = 'H' 111 | ERROR = 'X' 112 | 113 | def __init__(self, target_url=None, token=None, delay=3): 114 | 115 | # Get environmental variables if available. 116 | self.target_url = os.environ.get("PORTPLOW_URL", target_url) 117 | self.token = os.environ.get("PORTPLOW_TOKEN", token) 118 | try: 119 | self.delay = int(os.environ.get("PORTPLOW_DELAY", delay)) 120 | except ValueError: 121 | raise Exception("Delay is not an integer. Please fix.") 122 | 123 | if self.target_url is None: 124 | raise Exception("Target URL is not set.") 125 | 126 | if self.token is None: 127 | raise Exception("Token is not set.") 128 | 129 | self.log = logging.getLogger('job_monitor') 130 | 131 | # Process tracking variables 132 | self.process = None 133 | self.pid = None 134 | 135 | # Initial job object 136 | self.ready_state() 137 | 138 | # HTTP session 139 | self.s = requests.Session() 140 | self.s.headers.update({"Authentication": "ScannerToken {}".format(self.token), 141 | "Content-Type": "application/json"}) 142 | 143 | def get_job(self): 144 | """ 145 | Check if a job was returned. If so, kickoff job. 146 | :return: 147 | """ 148 | ''' Get job from response ''' 149 | 150 | if self.status != self.READY: 151 | self.log.debug("Skipping get_job since we aren't in a ready status.") 152 | return 153 | 154 | if self.response is None: 155 | self.log.debug("Skipping get_job because response is None.") 156 | return 157 | 158 | self.log.debug("See if a job was returned by the server.") 159 | 160 | if "jobs" not in self.response: 161 | self.log.debug("Jobs weren't in the response. Malformed response?") 162 | print("Response. {}".format(self.response)) 163 | elif len(self.response["jobs"]) > 0 and self.job is not None: 164 | self.log.debug("Jobs were returned but we already have one.") 165 | else: 166 | self.job = Job(**self.response['jobs'][0]) 167 | self.log.debug("New job received. {}" 168 | .format(json.dumps(self.response['jobs'][0]))) 169 | command = shlex.split(self.job.command) 170 | self.log.debug("Command to run: {}".format(command)) 171 | # command_log = os.path.join(self.job.output_dir, "output.log") 172 | self.tmp_stdout = tempfile.TemporaryFile() 173 | self.tmp_stderr = tempfile.TemporaryFile() 174 | self.process = Popen(command, 175 | # stdout=open(command_log, 'a+'), 176 | # stderr=STDOUT, 177 | stdout=self.tmp_stdout, 178 | stderr=self.tmp_stderr, 179 | bufsize=-1, 180 | cwd=self.job.output_dir) 181 | self.pid = self.process.pid 182 | self.job.status = self.EXECUTING 183 | self.status = self.IN_USE 184 | self.send_update() 185 | 186 | def send_update(self): 187 | """ 188 | Send updates to the server. 189 | """ 190 | self.log.info("Sending update to server.") 191 | 192 | data = {'status': self.status} 193 | 194 | if self.job is not None: 195 | data["jobs"] = [self.job.json_ready()] 196 | 197 | server_response = self.s.post(self.target_url, data=json.dumps(data)) 198 | if server_response.status_code == 200: 199 | response = server_response.json() 200 | if "message" in server_response: 201 | if response["message"] == "kill_all": 202 | self.process.kill() 203 | 204 | self.response = response 205 | self.log.debug("Server response returned {}".format(server_response.json())) 206 | else: 207 | self.log.error("Server had an issue. Returned ({}). Content: {}".format(server_response.status_code, server_response.content)) 208 | 209 | def check_status(self): 210 | """Check the status of the running process.""" 211 | self.log.debug("Checking process status") 212 | 213 | # Don't bother if we're not executing. 214 | if self.status != self.IN_USE: 215 | self.log.debug("No current jobs running.") 216 | return 217 | 218 | if self.process: 219 | self.log.debug("There is a process.") 220 | if self.process.poll() is not None: 221 | self.log.debug("Process {} is complete. Return code is {}".format(self.process.pid, self.process.returncode)) 222 | 223 | stdout, stderr = self.process.communicate() 224 | 225 | if self.process.returncode != 0: 226 | self.job.status = self.ERROR 227 | else: 228 | self.job.status = self.COMPLETE 229 | 230 | self.job.return_code = self.process.returncode 231 | self.tmp_stdout.seek(0) 232 | self.tmp_stderr.seek(0) 233 | self.job.stdout = self.tmp_stdout.read() 234 | self.job.stderr = self.tmp_stderr.read() 235 | 236 | # Blank out process and job 237 | self.process = None 238 | self.send_update() 239 | self.ready_state() 240 | else: 241 | self.log.debug("Process is still running.") 242 | 243 | def ready_state(self): 244 | """Reset job to defaults.""" 245 | self.status = self.READY 246 | self.job = None 247 | self.process = None 248 | self.response = None 249 | # self.data = {"status": self.READY, "jobs": [{}]} 250 | 251 | def start(self): 252 | """Starting loop function.""" 253 | self.send_update() 254 | 255 | while True: 256 | self.log.debug("Job status: {}. Running PID: {}".format(self.status, self.pid)) 257 | # data = json.dumps(self.data) 258 | # r = self.s.post(self.target_url, data=data) 259 | # self.response = r.json() 260 | self.check_status() 261 | self.send_update() 262 | self.get_job() 263 | sleep(self.delay) 264 | 265 | 266 | def run_client(): 267 | try: 268 | job_monitor = JobMonitor() 269 | job_monitor.start() 270 | except KeyboardInterrupt: 271 | sys.exit(0) 272 | except Exception as e: 273 | raise 274 | 275 | if __name__ == "__main__": 276 | run_client() 277 | -------------------------------------------------------------------------------- /portplow/configs/etc/circus/circus.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | endpoint = tcp://127.0.0.1:5555 3 | pubsub_endpoint = tcp://127.0.0.1:5556 4 | loglevel = WARNING 5 | ; stats_endpoint = tcp://127.0.0.1:5557 6 | ; statsd = False 7 | 8 | [watcher:app] 9 | cmd = /usr/local/bin/chaussette --fd $(circus.sockets.app) portplow.wsgi:application 10 | use_sockets = True 11 | numprocesses = 2 12 | priority = 2 13 | working_dir = /opt/portplow 14 | copy_env = True 15 | stdout_stream.class = FileStream 16 | stdout_stream.filename = /opt/portplow/logs/app_stdout.log 17 | stdout_stream.max_bytes = 1073741824 18 | stdout_stream.refresh_time = 0.3 19 | stderr_stream.class = FileStream 20 | stderr_stream.filename = /opt/portplow/logs/app_error.log 21 | stderr_stream.max_bytes = 1073741824 22 | stderr_stream.refresh_time = 0.3 23 | uid = portplow 24 | gid = portplow 25 | 26 | [watcher:dcelery] 27 | numprocesses = 1 28 | priority = 2 29 | working_dir = /opt/portplow 30 | copy_env = True 31 | cmd = /usr/local/bin/celery 32 | args = worker -A portplow -l INFO 33 | ; stderr_stream.class = FileStream 34 | ; stdout_stream.class = FileStream 35 | ; stdout_stream.filename = /opt/portplow/logs/celery-stdout.log 36 | ; stderr_stream.filename = /opt/portplow/logs/celery-stderr.log 37 | ; stdout_stream.refresh_time = 0.3 38 | ; stdout_stream.max_bytes = 1073741824 39 | ; stdout_stream.backup_count = 5 40 | uid = portplow 41 | gid = portplow 42 | 43 | [watcher:celerybeat] 44 | numprocesses = 1 45 | priority = 2 46 | working_dir = /opt/portplow 47 | copy_env = True 48 | cmd = /usr/local/bin/celery 49 | args = beat -A portplow -l INFO 50 | ;stderr_stream.class = FileStream 51 | ;stdout_stream.class = FileStream 52 | ;stdout_stream.filename = /opt/portplow/logs/celery-beat-stdout.log 53 | ;stderr_stream.filename = /opt/portplow/logs/celery-beat-stderr.log 54 | ;stdout_stream.refresh_time = 0.3 55 | ;stdout_stream.max_bytes = 1073741824 56 | ;stdout_stream.backup_count = 5 57 | uid = portplow 58 | gid = portplow 59 | 60 | [socket:app] 61 | host = 127.0.0.1 62 | port = 5558 63 | -------------------------------------------------------------------------------- /portplow/configs/etc/init/portplow.conf: -------------------------------------------------------------------------------- 1 | start on filesystem and net-device-up IFACE=lo 2 | stop on runlevel [016] 3 | 4 | respawn 5 | exec /usr/local/bin/circusd /etc/circus/circus.ini 6 | 7 | -------------------------------------------------------------------------------- /portplow/configs/etc/nginx/sites-available/default: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl; 3 | listen [::]:443 ssl; 4 | 5 | server_name --domain--; 6 | 7 | ssl_certificate /etc/letsencrypt/live/--domain--/fullchain.pem; 8 | ssl_certificate_key /etc/letsencrypt/live/--domain--/privkey.pem; 9 | 10 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 11 | ssl_prefer_server_ciphers on; 12 | ssl_dhparam /etc/ssl/certs/dhparam.pem; 13 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 14 | ssl_session_timeout 1d; 15 | ssl_session_cache shared:SSL:50m; 16 | ssl_stapling on; 17 | ssl_stapling_verify on; 18 | add_header Strict-Transport-Security max-age=15768000; 19 | 20 | location /static { 21 | autoindex off; 22 | alias /opt/portplow/static/; 23 | } 24 | 25 | location / { 26 | proxy_pass http://127.0.0.1:5558/; 27 | proxy_pass_header Server; 28 | proxy_set_header Host $host; 29 | proxy_redirect off; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 32 | proxy_set_header X-Scheme $scheme; 33 | proxy_connect_timeout 600; 34 | proxy_send_timeout 600; 35 | proxy_read_timeout 600; 36 | real_ip_header X-Real-IP; 37 | } 38 | 39 | location ~ /.well-known { 40 | allow all; 41 | root /var/www/html; 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /portplow/configs/portplow.conf: -------------------------------------------------------------------------------- 1 | [digitalocean] 2 | api_key = --api-token-- 3 | 4 | [external] 5 | ip = --public-ip-- 6 | port = 443 7 | domain = --domain-- 8 | 9 | [email] 10 | use_tls = True 11 | from_email = support@domain.io 12 | server_email = support@domain.io 13 | host = mail.privateemail.com 14 | port = 26 15 | user = --email-user-- 16 | password = --email-pass-- 17 | 18 | [database] 19 | user = portplow 20 | password = --db-pass-- 21 | database = portplow 22 | 23 | [internal] 24 | site_name = internaldomain.io 25 | secret_key = --secret-key-- -------------------------------------------------------------------------------- /portplow/configs/portplow.conf.default: -------------------------------------------------------------------------------- 1 | [digitalocean] 2 | api_key = --api-token-- 3 | 4 | [external] 5 | ip = --public-ip-- 6 | port = 443 7 | domain = --domain-- 8 | 9 | [email] 10 | use_tls = True 11 | from_email = support@domain.io 12 | server_email = support@domain.io 13 | host = mail.privateemail.com 14 | port = 26 15 | user = --email-user-- 16 | password = --email-pass-- 17 | 18 | [database] 19 | user = portplow 20 | password = --db-pass-- 21 | database = portplow 22 | 23 | [internal] 24 | site_name = internaldomain.io 25 | secret_key = --secret-key-- -------------------------------------------------------------------------------- /portplow/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portplow.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /portplow/portplow/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .celery import app as celery_app 4 | -------------------------------------------------------------------------------- /portplow/portplow/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from celery import Celery 6 | 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'portplow.settings') 8 | 9 | from django.conf import settings # noqa 10 | 11 | app = Celery('portplow') 12 | 13 | # Using a string here means the worker will not have to 14 | # pickle the object when using Windows. 15 | app.config_from_object('django.conf:settings') 16 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 17 | 18 | 19 | @app.task(bind=True) 20 | def debug_task(self): 21 | print('Request: {0!r}'.format(self.request)) -------------------------------------------------------------------------------- /portplow/portplow/middleware.py: -------------------------------------------------------------------------------- 1 | from django.db import connection 2 | from django.template import Template, Context 3 | 4 | # 5 | # Log all SQL statements direct to the console (when running in DEBUG) 6 | # Intended for use with the django development server. 7 | # 8 | 9 | 10 | class SQLLogToConsoleMiddleware: 11 | def process_response(self, request, response): 12 | print("SQLMiddleware") 13 | if connection.queries: 14 | time = sum([float(q['time']) for q in connection.queries]) 15 | t = Template("{{count}} quer{{count|pluralize:\"y,ies\"}} in {{time}} seconds:\n\n{% for sql in sqllog %}[{{forloop.counter}}] {{sql.time}}s: {{sql.sql|safe}}{% if not forloop.last %}\n\n{% endif %}{% endfor %}") 16 | print(t.render(Context({'sqllog':connection.queries,'count':len(connection.queries),'time':time}))) 17 | return response 18 | -------------------------------------------------------------------------------- /portplow/portplow/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | license... 3 | """ 4 | import os 5 | import sys 6 | import logging 7 | from datetime import timedelta 8 | from django.contrib.messages import constants as messages 9 | 10 | try: 11 | import configparser 12 | except ImportError: 13 | import ConfigParser as configparser 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | BROKER_URL = 'redis://localhost:6379/0' 18 | CELERY_ACCEPT_CONTENT = ['json'] 19 | CELERY_TASK_SERIALIZER = 'json' 20 | CELERY_RESULT_SERIALIZER = 'json' 21 | CELERY_TIMEZONE = 'UTC' 22 | 23 | CELERYBEAT_SCHEDULE = { 24 | 'update-progress-counts': { 25 | 'task': 'scanner.tasks.update_progress_counts', 26 | 'schedule': timedelta(seconds=300) 27 | }, 28 | 'cleanup-completed-scans': { 29 | 'task': 'scanner.tasks.cleanup_completed_scans', 30 | 'schedule': timedelta(seconds=120) 31 | }, 32 | 'load-job-queues': { 33 | 'task': 'scanner.tasks.load_job_queues', 34 | 'schedule': timedelta(seconds=60) 35 | }, 36 | 'update-completion-dates': { 37 | 'task': 'scanner.tasks.update_completion_dates', 38 | 'schedule': timedelta(seconds=3600) 39 | } 40 | } 41 | 42 | config = configparser.ConfigParser() 43 | config.read(os.path.join(os.path.expanduser('~'), ".portplow.conf")) 44 | 45 | EXTERNAL_IP = config.get("external", "ip") 46 | EXTERNAL_PORT = config.get("external", "port") 47 | DOMAIN = config.get("external", "domain") 48 | SITE_NAME = config.get("internal", "site_name") 49 | 50 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 51 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 52 | 53 | 54 | SECRET_KEY = config.get("internal", "secret_key") 55 | DEBUG = False 56 | 57 | ALLOWED_HOSTS = [DOMAIN, '0.0.0.0', EXTERNAL_IP] 58 | 59 | MAX_JOB_RETRIES = 10 60 | 61 | # Application definition 62 | 63 | INSTALLED_APPS = [ 64 | 'django.contrib.admin', 65 | 'django.contrib.auth', 66 | 'django.contrib.contenttypes', 67 | 'django.contrib.sessions', 68 | 'django.contrib.messages', 69 | 'django.contrib.staticfiles', 70 | 'django_extensions', 71 | 'rest_framework', 72 | 'rest_framework.authtoken', 73 | 'bootstrap3', 74 | 'crispy_forms', 75 | 'scanner', 76 | 'api', 77 | 'utils', 78 | 'session_security', 79 | ] 80 | 81 | MIDDLEWARE_CLASSES = [ 82 | 'django.middleware.security.SecurityMiddleware', 83 | 'django.contrib.sessions.middleware.SessionMiddleware', 84 | 'django.middleware.common.CommonMiddleware', 85 | 'django.middleware.csrf.CsrfViewMiddleware', 86 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 87 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 88 | 'session_security.middleware.SessionSecurityMiddleware', 89 | 'django.contrib.messages.middleware.MessageMiddleware', 90 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 91 | ] 92 | 93 | ROOT_URLCONF = 'portplow.urls' 94 | 95 | TEMPLATES = [ 96 | { 97 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 98 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 99 | , 100 | 'APP_DIRS': True, 101 | 'OPTIONS': { 102 | 'context_processors': [ 103 | 'django.template.context_processors.debug', 104 | 'django.template.context_processors.request', 105 | "django.core.context_processors.request", 106 | 'django.contrib.auth.context_processors.auth', 107 | 'django.contrib.messages.context_processors.messages', 108 | # 'django.contrib.auth.context_processors.PermWrapper', 109 | ], 110 | }, 111 | }, 112 | ] 113 | 114 | WSGI_APPLICATION = 'portplow.wsgi.application' 115 | 116 | DATABASES = { 117 | # 'default': { 118 | # 'ENGINE': 'django.db.backends.sqlite3', 119 | # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 120 | # } 121 | 'default': { 122 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 123 | 'NAME': config.get("database", "database"), 124 | 'USER': config.get("database", "user"), 125 | 'PASSWORD': config.get("database", "password"), 126 | 'HOST': 'localhost', 127 | 'PORT': '' 128 | } 129 | 130 | } 131 | 132 | MESSAGE_TAGS = { 133 | messages.ERROR: 'danger' 134 | } 135 | 136 | AUTH_PASSWORD_VALIDATORS = [ 137 | {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',}, 138 | {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',}, 139 | {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',}, 140 | {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',}, 141 | ] 142 | 143 | LANGUAGE_CODE = 'en-us' 144 | TIME_ZONE = 'UTC' 145 | USE_I18N = True 146 | USE_L10N = True 147 | USE_TZ = False 148 | 149 | STATIC_URL = '/static/' 150 | STATICFILES_FINDERS = ["django.contrib.staticfiles.finders.FileSystemFinder", 151 | "django.contrib.staticfiles.finders.AppDirectoriesFinder"] 152 | 153 | STATIC_ROOT = '/opt/portplow/static' 154 | 155 | REST_FRAMEWORK = { 156 | 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', 157 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 158 | 'rest_framework.authentication.SessionAuthentication', 159 | 'rest_framework.authentication.TokenAuthentication', 160 | 'rest_framework.authentication.BasicAuthentication', 161 | ), 162 | # 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), 163 | 'PAGE_SIZE': 100 164 | } 165 | 166 | 167 | # Attempt to get DigitalOcean keys from environment 168 | DO_USERNAME = os.environ.get("PORTPLOW_DOUSER", "portplow") 169 | try: 170 | config = configparser.ConfigParser() 171 | config.read(os.path.join(os.path.expanduser('~'), ".portplow.conf")) 172 | DO_APIKEY = os.environ.get("PORTPLOW_DOAPIKEY", None) 173 | if DO_APIKEY is None: 174 | DO_APIKEY = config.get("digitalocean", "api_key") 175 | except configparser.NoSectionError: 176 | log.error("~/.portplow.conf is missing the [digitalocean] section.") 177 | sys.exit(1) 178 | except configparser.NoOptionError: 179 | log.error("~/.portplow.conf is missing the 'api_key =' option.") 180 | sys.exit(1) 181 | 182 | # print("API key: {}".format(DO_APIKEY)) 183 | 184 | CLIENT_OUTPUT_DIR = "/var/opt/portplow/results" 185 | CLIENT_DELAY = 3 186 | 187 | LOGGING = { 188 | 'version': 1, 189 | 'disable_existing_loggers': False, 190 | 'formatters': { 191 | 'verbose': { 192 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 193 | }, 194 | 'simple': { 195 | 'format': '%(levelname)s %(message)s' 196 | }, 197 | }, 198 | 'handlers': { 199 | # 'file': { 200 | # 'level': 'DEBUG', 201 | # 'class': 'logging.FileHandler', 202 | # 'filename': 'logs/portplow.log', 203 | # 'formatter': 'verbose' 204 | # }, 205 | # 'dbfile': { 206 | # 'level': 'DEBUG', 207 | # 'class': 'logging.FileHandler', 208 | # 'filename': 'logs/database-queries.log', 209 | # 'formatter': 'verbose' 210 | # }, 211 | 'console': { 212 | 'level': 'INFO', 213 | 'class': 'logging.StreamHandler', 214 | 'formatter': 'simple' 215 | } 216 | }, 217 | 'loggers': { 218 | 'django': { 219 | 'handlers': [ 220 | # 'file', 221 | 'console' 222 | ], 223 | 'propagate': True, 224 | }, 225 | # 'django.db.backends': { 226 | # 'handlers': ['dbfile'], 227 | # 'level': 'DEBUG', 228 | # }, 229 | }, 230 | } 231 | 232 | LOGIN_URL = '/portplow/login' 233 | LOGIN_REDIRECT_URL = '/portplow/' 234 | 235 | CRISPY_TEMPLATE_PACK = 'bootstrap3' 236 | 237 | EMAIL_USE_TLS = config.get("email", "use_tls") 238 | DEFAULT_FROM_EMAIL = config.get("email", "from_email") 239 | SERVER_EMAIL = config.get("email", "server_email") 240 | EMAIL_HOST = config.get("email", "host") 241 | EMAIL_PORT = config.get("email", "port") 242 | EMAIL_HOST_USER = config.get("email", "user") 243 | EMAIL_HOST_PASSWORD = config.get("email", "password") 244 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 245 | 246 | ''' 247 | use_tls = True 248 | from_email = 'support@portplow.io' 249 | server_email = 'support@portplow.io' 250 | host = 'mail.privateemail.com' 251 | port = 26 252 | user = 'support@portplow.io' 253 | password = '--email-password--' 254 | ''' 255 | 256 | # def show_toolbar(request): 257 | # # print("Checking if we should show the toolbar. User:{}".format(request.user)) 258 | # return not request.is_ajax() and request.user.username == "drush" 259 | # 260 | # DEBUG_TOOLBAR_CONFIG = { 261 | # 'SHOW_TOOLBAR_CALLBACK': 'portplow.settings.show_toolbar', 262 | # } 263 | # 264 | # DEBUG_TOOLBAR_PANELS = [ 265 | # # 'debug_toolbar.panels.versions.VersionsPanel', 266 | # 'debug_toolbar.panels.timer.TimerPanel', 267 | # 'debug_toolbar.panels.settings.SettingsPanel', 268 | # 'debug_toolbar.panels.headers.HeadersPanel', 269 | # 'debug_toolbar.panels.request.RequestPanel', 270 | # 'debug_toolbar.panels.sql.SQLPanel', 271 | # # 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 272 | # 'debug_toolbar.panels.templates.TemplatesPanel', 273 | # 'debug_toolbar.panels.cache.CachePanel', 274 | # 'debug_toolbar.panels.signals.SignalsPanel', 275 | # 'debug_toolbar.panels.logging.LoggingPanel', 276 | # 'debug_toolbar.panels.redirects.RedirectsPanel', 277 | # ] 278 | 279 | CACHES = { 280 | "default": { 281 | "BACKEND": "django_redis.cache.RedisCache", 282 | "LOCATION": BROKER_URL, 283 | "OPTIONS": { 284 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 285 | } 286 | } 287 | } 288 | 289 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 290 | SESSION_CACHE_ALIAS = "default" 291 | 292 | SESSION_SECURITY_WARN_AFTER = 540 293 | SESSION_SECURITY_EXPIRE_AFTER = 600 294 | -------------------------------------------------------------------------------- /portplow/portplow/settings.py.default: -------------------------------------------------------------------------------- 1 | """ 2 | license... 3 | """ 4 | import os 5 | import sys 6 | import logging 7 | from datetime import timedelta 8 | from django.contrib.messages import constants as messages 9 | 10 | try: 11 | import configparser 12 | except ImportError: 13 | import ConfigParser as configparser 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | BROKER_URL = 'redis://localhost:6379/0' 18 | CELERY_ACCEPT_CONTENT = ['json'] 19 | CELERY_TASK_SERIALIZER = 'json' 20 | CELERY_RESULT_SERIALIZER = 'json' 21 | CELERY_TIMEZONE = 'UTC' 22 | 23 | CELERYBEAT_SCHEDULE = { 24 | 'update-progress-counts': { 25 | 'task': 'scanner.tasks.update_progress_counts', 26 | 'schedule': timedelta(seconds=300) 27 | }, 28 | 'cleanup-completed-scans': { 29 | 'task': 'scanner.tasks.cleanup_completed_scans', 30 | 'schedule': timedelta(seconds=120) 31 | }, 32 | 'load-job-queues': { 33 | 'task': 'scanner.tasks.load_job_queues', 34 | 'schedule': timedelta(seconds=60) 35 | }, 36 | 'update-completion-dates': { 37 | 'task': 'scanner.tasks.update_completion_dates', 38 | 'schedule': timedelta(seconds=3600) 39 | } 40 | } 41 | 42 | EXTERNAL_IP = "104.131.255.255" 43 | EXTERNAL_PORT = 443 44 | 45 | DOMAIN = 'your.domain.io' 46 | SITE_NAME = 'domain.io' 47 | 48 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 49 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 50 | 51 | 52 | # Quick-start development settings - unsuitable for production 53 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 54 | 55 | # SECURITY WARNING: keep the secret key used in production secret! 56 | SECRET_KEY = 'K34567eirutyqwertyuiop[]_yxwvut987654321' 57 | 58 | # SECURITY WARNING: don't run with debug turned on in production! 59 | DEBUG = False 60 | 61 | ALLOWED_HOSTS = ['your.domain.io', '0.0.0.0', '104.131.255.255'] 62 | 63 | MAX_JOB_RETRIES = 10 64 | 65 | # Application definition 66 | 67 | INSTALLED_APPS = [ 68 | 'django.contrib.admin', 69 | 'django.contrib.auth', 70 | 'django.contrib.contenttypes', 71 | 'django.contrib.sessions', 72 | 'django.contrib.messages', 73 | 'django.contrib.staticfiles', 74 | # 'debug_toolbar', 75 | 'django_extensions', 76 | # 'kombu.transport.django', 77 | 'rest_framework', 78 | 'rest_framework.authtoken', 79 | 'bootstrap3', 80 | 'crispy_forms', 81 | 'scanner', 82 | 'api', 83 | 'utils', 84 | 'session_security', 85 | # 'memoize', 86 | ] 87 | 88 | MIDDLEWARE_CLASSES = [ 89 | # 'portplow.middleware.SQLLogToConsoleMiddleware', 90 | 'django.middleware.security.SecurityMiddleware', 91 | 'django.contrib.sessions.middleware.SessionMiddleware', 92 | 'django.middleware.common.CommonMiddleware', 93 | 'django.middleware.csrf.CsrfViewMiddleware', 94 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 95 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 96 | 'session_security.middleware.SessionSecurityMiddleware', 97 | # 'debug_toolbar.middleware.DebugToolbarMiddleware', 98 | 'django.contrib.messages.middleware.MessageMiddleware', 99 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 100 | ] 101 | 102 | ROOT_URLCONF = 'portplow.urls' 103 | 104 | TEMPLATES = [ 105 | { 106 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 107 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 108 | , 109 | 'APP_DIRS': True, 110 | 'OPTIONS': { 111 | 'context_processors': [ 112 | 'django.template.context_processors.debug', 113 | 'django.template.context_processors.request', 114 | "django.core.context_processors.request", 115 | 'django.contrib.auth.context_processors.auth', 116 | 'django.contrib.messages.context_processors.messages', 117 | # 'django.contrib.auth.context_processors.PermWrapper', 118 | ], 119 | }, 120 | }, 121 | ] 122 | 123 | WSGI_APPLICATION = 'portplow.wsgi.application' 124 | 125 | DATABASES = { 126 | # 'default': { 127 | # 'ENGINE': 'django.db.backends.sqlite3', 128 | # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 129 | # } 130 | 'default': { 131 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 132 | 'NAME': 'portplow', 133 | 'USER': 'portplow', 134 | 'PASSWORD': 'Thisisyourpasswordhere', 135 | 'HOST': 'localhost', 136 | 'PORT': '' 137 | } 138 | 139 | } 140 | 141 | MESSAGE_TAGS = { 142 | messages.ERROR: 'danger' 143 | } 144 | 145 | AUTH_PASSWORD_VALIDATORS = [ 146 | {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',}, 147 | {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',}, 148 | {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',}, 149 | {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',}, 150 | ] 151 | 152 | LANGUAGE_CODE = 'en-us' 153 | TIME_ZONE = 'UTC' 154 | USE_I18N = True 155 | USE_L10N = True 156 | USE_TZ = False 157 | 158 | STATIC_URL = '/static/' 159 | STATICFILES_FINDERS = ["django.contrib.staticfiles.finders.FileSystemFinder", 160 | "django.contrib.staticfiles.finders.AppDirectoriesFinder"] 161 | 162 | STATIC_ROOT = '/opt/portplow/static' 163 | 164 | REST_FRAMEWORK = { 165 | 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', 166 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 167 | 'rest_framework.authentication.SessionAuthentication', 168 | 'rest_framework.authentication.TokenAuthentication', 169 | 'rest_framework.authentication.BasicAuthentication', 170 | ), 171 | # 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), 172 | 'PAGE_SIZE': 100 173 | } 174 | 175 | 176 | # Attempt to get DigitalOcean keys from environment 177 | DO_USERNAME = os.environ.get("PORTPLOW_DOUSER", "portplow") 178 | try: 179 | config = configparser.ConfigParser() 180 | config.read(os.path.join(os.path.expanduser('~'), ".portplow.conf")) 181 | DO_APIKEY = os.environ.get("PORTPLOW_DOAPIKEY", None) 182 | if DO_APIKEY is None: 183 | DO_APIKEY = config.get("digitalocean", "api_key") 184 | except configparser.NoSectionError: 185 | log.error("~/.portplow.conf is missing the [digitalocean] section.") 186 | sys.exit(1) 187 | except configparser.NoOptionError: 188 | log.error("~/.portplow.conf is missing the 'api_key =' option.") 189 | sys.exit(1) 190 | 191 | # print("API key: {}".format(DO_APIKEY)) 192 | 193 | CLIENT_OUTPUT_DIR = "/var/opt/portplow/results" 194 | CLIENT_DELAY = 3 195 | 196 | LOGGING = { 197 | 'version': 1, 198 | 'disable_existing_loggers': False, 199 | 'formatters': { 200 | 'verbose': { 201 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 202 | }, 203 | 'simple': { 204 | 'format': '%(levelname)s %(message)s' 205 | }, 206 | }, 207 | 'handlers': { 208 | # 'file': { 209 | # 'level': 'DEBUG', 210 | # 'class': 'logging.FileHandler', 211 | # 'filename': 'logs/portplow.log', 212 | # 'formatter': 'verbose' 213 | # }, 214 | # 'dbfile': { 215 | # 'level': 'DEBUG', 216 | # 'class': 'logging.FileHandler', 217 | # 'filename': 'logs/database-queries.log', 218 | # 'formatter': 'verbose' 219 | # }, 220 | 'console': { 221 | 'level': 'INFO', 222 | 'class': 'logging.StreamHandler', 223 | 'formatter': 'simple' 224 | } 225 | }, 226 | 'loggers': { 227 | 'django': { 228 | 'handlers': [ 229 | # 'file', 230 | 'console' 231 | ], 232 | 'propagate': True, 233 | }, 234 | # 'django.db.backends': { 235 | # 'handlers': ['dbfile'], 236 | # 'level': 'DEBUG', 237 | # }, 238 | }, 239 | } 240 | 241 | LOGIN_URL = '/portplow/login' 242 | LOGIN_REDIRECT_URL = '/portplow/' 243 | 244 | CRISPY_TEMPLATE_PACK = 'bootstrap3' 245 | 246 | EMAIL_USE_TLS = True 247 | DEFAULT_FROM_EMAIL = 'support@domain.io' 248 | SERVER_EMAIL = 'support@domain.io' 249 | EMAIL_HOST = 'mail.privateemail.com' 250 | EMAIL_PORT = 26 251 | EMAIL_HOST_USER = 'support@domain.io' 252 | EMAIL_HOST_PASSWORD = 'Thisisareallysimplepassword1' 253 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 254 | 255 | # def show_toolbar(request): 256 | # # print("Checking if we should show the toolbar. User:{}".format(request.user)) 257 | # return not request.is_ajax() and request.user.username == "drush" 258 | # 259 | # DEBUG_TOOLBAR_CONFIG = { 260 | # 'SHOW_TOOLBAR_CALLBACK': 'portplow.settings.show_toolbar', 261 | # } 262 | # 263 | # DEBUG_TOOLBAR_PANELS = [ 264 | # # 'debug_toolbar.panels.versions.VersionsPanel', 265 | # 'debug_toolbar.panels.timer.TimerPanel', 266 | # 'debug_toolbar.panels.settings.SettingsPanel', 267 | # 'debug_toolbar.panels.headers.HeadersPanel', 268 | # 'debug_toolbar.panels.request.RequestPanel', 269 | # 'debug_toolbar.panels.sql.SQLPanel', 270 | # # 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 271 | # 'debug_toolbar.panels.templates.TemplatesPanel', 272 | # 'debug_toolbar.panels.cache.CachePanel', 273 | # 'debug_toolbar.panels.signals.SignalsPanel', 274 | # 'debug_toolbar.panels.logging.LoggingPanel', 275 | # 'debug_toolbar.panels.redirects.RedirectsPanel', 276 | # ] 277 | 278 | CACHES = { 279 | "default": { 280 | "BACKEND": "django_redis.cache.RedisCache", 281 | "LOCATION": BROKER_URL, 282 | "OPTIONS": { 283 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 284 | } 285 | } 286 | } 287 | 288 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 289 | SESSION_CACHE_ALIAS = "default" 290 | 291 | SESSION_SECURITY_WARN_AFTER=540 292 | SESSION_SECURITY_EXPIRE_AFTER=600 293 | -------------------------------------------------------------------------------- /portplow/portplow/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include, patterns 2 | from django.shortcuts import HttpResponseRedirect, resolve_url 3 | from rest_framework.authtoken.views import obtain_auth_token 4 | import debug_toolbar 5 | 6 | from api.urls import router 7 | from api.views import checkin 8 | from scanner import urls as scanner_urls 9 | from utils import urls as util_urls 10 | 11 | urlpatterns = [ 12 | url(r'^$', lambda r: HttpResponseRedirect(resolve_url('portplow:scan-list'))), 13 | url(r'^api/token/', obtain_auth_token, name='api-token'), 14 | url(r'^api/', include(router.urls, namespace="api")), 15 | url(r'^portplow/', include(scanner_urls.urlpatterns, namespace="portplow")), 16 | url(r'^account/', include(util_urls.urlpatterns, namespace='utils')), 17 | url(r'^checkin/(?P[0-9a-zA-Z-]+)/', checkin, name='checkin-scanner'), 18 | url(r'^__debug__/', include(debug_toolbar.urls)), 19 | url(r'session_security/', include('session_security.urls')), 20 | ] 21 | -------------------------------------------------------------------------------- /portplow/portplow/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for portplow project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portplow.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /portplow/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.3 2 | apache-libcloud==0.20.1 3 | celery==3.1.23 4 | chaussette==1.3.0 5 | circus==0.13.0 6 | django-bootstrap3==7.0.1 7 | django-crispy-forms==1.6.0 8 | django-debug-toolbar==1.4 9 | django-extensions==1.6.1 10 | django-filter==0.12.0 11 | django-memoize==1.3.1 12 | django-redis==4.4.2 13 | django-session-security==2.3.2 14 | djangorestframework==3.3.2 15 | paramiko~> 1.17.6 16 | psutil==4.1.0 17 | psycopg2==2.6.1 18 | git+https://github.com/dlitz/pycrypto.git@v2.7a1 19 | # pycrypto==2.7a1 20 | python-libnmap==0.7.0 21 | -------------------------------------------------------------------------------- /portplow/scanner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/scanner/__init__.py -------------------------------------------------------------------------------- /portplow/scanner/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /portplow/scanner/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ScannerConfig(AppConfig): 5 | name = 'scanner' 6 | -------------------------------------------------------------------------------- /portplow/scanner/basicauth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Original code from: 3 | https://www.djangosnippets.org/snippets/243/ 4 | """ 5 | import base64 6 | 7 | from django.http import HttpResponse 8 | from django.contrib.auth import authenticate, login 9 | 10 | 11 | def view_or_basicauth(view, request, test_func, realm="", *args, **kwargs): 12 | """ 13 | This is a helper function used by both 'logged_in_or_basicauth' and 14 | 'has_perm_or_basicauth' that does the nitty of determining if they 15 | are already logged in or if they have provided proper http-authorization 16 | and returning the view if all goes well, otherwise responding with a 401. 17 | """ 18 | if getattr(request, "user", False): 19 | # Already logged in, just return the view. 20 | # 21 | return view(request, *args, **kwargs) 22 | 23 | # They are not logged in. See if they provided login credentials 24 | # 25 | if 'HTTP_AUTHORIZATION' in request.META: 26 | auth = request.META['HTTP_AUTHORIZATION'].split() 27 | if len(auth) == 2: 28 | # NOTE: We are only support basic authentication for now. 29 | # 30 | if auth[0].lower() == "basic": 31 | uname, passwd = base64.b64decode(auth[1]).decode('utf-8').split(':', 1) 32 | user = authenticate(username=uname, password=passwd) 33 | if user is not None: 34 | if user.is_active: 35 | login(request, user) 36 | request.user = user 37 | if test_func(request.user): 38 | return view(request, *args, **kwargs) 39 | 40 | # Either they did not provide an authorization header or 41 | # something in the authorization attempt failed. Send a 401 42 | # back to them to ask them to authenticate. 43 | # 44 | response = HttpResponse() 45 | response.status_code = 401 46 | response['WWW-Authenticate'] = 'Basic realm="%s"' % realm 47 | return response 48 | 49 | 50 | def logged_in_or_basicauth(realm=""): 51 | """ 52 | A simple decorator that requires a user to be logged in. If they are not 53 | logged in the request is examined for a 'authorization' header. 54 | 55 | If the header is present it is tested for basic authentication and 56 | the user is logged in with the provided credentials. 57 | 58 | If the header is not present a http 401 is sent back to the 59 | requestor to provide credentials. 60 | 61 | The purpose of this is that in several django projects I have needed 62 | several specific views that need to support basic authentication, yet the 63 | web site as a whole used django's provided authentication. 64 | 65 | The uses for this are for urls that are access programmatically such as 66 | by rss feed readers, yet the view requires a user to be logged in. Many rss 67 | readers support supplying the authentication credentials via http basic 68 | auth (and they do NOT support a redirect to a form where they post a 69 | username/password.) 70 | 71 | Use is simple: 72 | 73 | @logged_in_or_basicauth() 74 | def your_view: 75 | ... 76 | 77 | You can provide the name of the realm to ask for authentication within. 78 | """ 79 | def view_decorator(func): 80 | def wrapper(request, *args, **kwargs): 81 | return view_or_basicauth(func, request, 82 | lambda u: u.is_authenticated(), 83 | realm, *args, **kwargs) 84 | return wrapper 85 | return view_decorator 86 | 87 | 88 | def has_perm_or_basicauth(perm, realm = ""): 89 | """ 90 | This is similar to the above decorator 'logged_in_or_basicauth' 91 | except that it requires the logged in user to have a specific 92 | permission. 93 | 94 | Use: 95 | 96 | @logged_in_or_basicauth('asforums.view_forumcollection') 97 | def your_view: 98 | ... 99 | 100 | """ 101 | def view_decorator(func): 102 | def wrapper(request, *args, **kwargs): 103 | return view_or_basicauth(func, request, 104 | lambda u: u.has_perm(perm), 105 | realm, *args, **kwargs) 106 | return wrapper 107 | return view_decorator 108 | -------------------------------------------------------------------------------- /portplow/scanner/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import Form, ModelForm 2 | from django.forms.fields import TextInput, HiddenInput, CharField 3 | from crispy_forms.helper import FormHelper 4 | from crispy_forms.layout import Layout, Submit, Button, Field, Hidden 5 | from crispy_forms.bootstrap import FormActions, AppendedText, TabHolder, Tab 6 | from scanner.models import Scan, Profile 7 | 8 | 9 | class ScanForm(ModelForm): 10 | 11 | start_date = CharField(widget=HiddenInput()) 12 | stop_date = CharField(widget=HiddenInput()) 13 | scan_range = CharField( 14 | label="Scan Range", 15 | widget=TextInput(attrs={'class': 'dtrange'}) 16 | ) 17 | 18 | def __init__(self, *args, **kwargs): 19 | super(ScanForm, self).__init__(*args, **kwargs) 20 | self.helper = FormHelper() 21 | self.helper.help_text_inline = True 22 | self.helper.form_class = 'form-horizontal' 23 | self.helper.label_class = 'col-lg-2' 24 | self.helper.field_class = 'col-lg-8' 25 | self.helper.layout = Layout( 26 | TabHolder( 27 | Tab('Details', 28 | 'name', 'profile', 'group', 'hosts'), 29 | Tab('Options', 30 | 'chunk_size', 'scanner_count', 31 | 'scan_hours', 32 | 'scan_range', 33 | 'start_date', 34 | 'end_date', 35 | ), 36 | Tab('Deconfliction', 37 | 'deconfliction_message', 38 | 'htaccess') 39 | ), 40 | FormActions( 41 | Submit('create', 'Create Scan'), 42 | Button('cancel', 'Cancel') 43 | ) 44 | 45 | ) 46 | 47 | class Meta: 48 | model = Scan 49 | fields = ['name', 'hosts', 'profile', 'group', 'chunk_size', 'scanner_count', 'deconfliction_message', 50 | 'scan_hours', 'start_date', 'stop_date', 'htaccess'] 51 | 52 | 53 | class ProfileForm(ModelForm): 54 | 55 | command = CharField( 56 | label="Command Line", 57 | widget=TextInput(attrs={'class': ''}) 58 | ) 59 | 60 | def __init__(self, *args, **kwargs): 61 | super(ProfileForm, self).__init__(*args, **kwargs) 62 | self.helper = FormHelper() 63 | self.helper.form_class = 'form-horizontal' 64 | self.helper.label_class = 'col-lg-2' 65 | self.helper.field_class = 'col-lg-8' 66 | self.helper.layout = Layout( 67 | 'name', 68 | 'tool', 69 | 'command', 70 | 'description', 71 | FormActions( 72 | Submit('add', 'Add Profile'), 73 | Button('cancel', 'Cancel') 74 | ) 75 | ) 76 | 77 | class Meta: 78 | model = Profile 79 | fields = ['name', 'tool', 'command', 'description'] 80 | -------------------------------------------------------------------------------- /portplow/scanner/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/scanner/management/__init__.py -------------------------------------------------------------------------------- /portplow/scanner/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/scanner/management/commands/__init__.py -------------------------------------------------------------------------------- /portplow/scanner/management/commands/add_scanner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | from uuid import UUID 4 | from django.core.management.base import BaseCommand, CommandError 5 | from scanner.models import Scan 6 | from scanner.signals import add_scanner 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Add specified number of scanners to scan' 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument('--num', nargs="?", type=int, help="Number of scanners to add") 14 | parser.add_argument('--scan', type=UUID, help="Scan UUID") 15 | 16 | def handle(self, *args, **options): 17 | if options['scan'] is None: 18 | raise CommandError("Scan ID was not specified.") 19 | 20 | num_scanners = 1 21 | if options['num'] is not None: 22 | if options['num'] <= 0: 23 | raise CommandError("You must have a positive number of scanners to add.") 24 | num_scanners = options['num'] 25 | 26 | try: 27 | scan = Scan.objects.get(id=options['scan']) 28 | except Scan.DoesNotExist: 29 | raise CommandError("Specified scan ID {} does not exist.".format(options['scan'])) 30 | 31 | if scan.status == Scan.COMPLETE: 32 | raise CommandError("Cannot add scanner to an already completed job.") 33 | 34 | for x in range(num_scanners): 35 | add_scanner(scan) 36 | 37 | self.stdout.write(self.style.SUCCESS("Successfully added {} scanners to scan {}.".format(num_scanners, scan.name))) 38 | 39 | -------------------------------------------------------------------------------- /portplow/scanner/management/commands/export_scan.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | from uuid import UUID 4 | from django.core.management.base import BaseCommand, CommandError 5 | from scanner.models import Scan 6 | from scanner.tasks import export_scan 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Export raw data for scan' 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument('--scan', type=UUID, help="Scan UUID") 14 | 15 | def handle(self, *args, **options): 16 | if options['scan'] is None: 17 | raise CommandError("Scan ID was not specified.") 18 | 19 | try: 20 | scan = Scan.objects.get(id=options['scan']) 21 | except Scan.DoesNotExist: 22 | raise CommandError("Specified scan ID {} does not exist.".format(options['scan'])) 23 | 24 | export_scan(scan.id) 25 | 26 | self.stdout.write(self.style.SUCCESS("Successfully exported scan {}.".format(scan.name))) 27 | 28 | -------------------------------------------------------------------------------- /portplow/scanner/management/commands/get_key.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | from uuid import UUID 4 | from django.core.management.base import BaseCommand, CommandError 5 | from scanner.models import Scanner 6 | 7 | 8 | class Command(BaseCommand): 9 | help = 'Get private SSH keys for scanners and save them to /tmp' 10 | 11 | def add_arguments(self, parser): 12 | parser.add_argument('--ip', nargs="*", type=str, help="IP address of scanner") 13 | parser.add_argument('--id', nargs="*", type=UUID, help="UUID of scanner") 14 | parser.add_argument('--scan', nargs="*", type=UUID, help="Scan UUID") 15 | 16 | def parse_keys(self, scanners): 17 | for scanner in scanners: 18 | if scanner.ip is not None: 19 | output_file = os.path.join('/', 'tmp', "{}__{}".format(scanner.scan.id, scanner.ip)) 20 | else: 21 | output_file = os.path.join('/', 'tmp', "{}__{}".format(scanner.scan.id, scanner.id)) 22 | 23 | with open(output_file, "w") as f: 24 | result = f.write(scanner.key) 25 | 26 | os.chmod(output_file, stat.S_IRUSR | stat.S_IWUSR) 27 | 28 | def handle(self, *args, **options): 29 | if options['ip'] is not None: 30 | scanners = Scanner.objects.filter(ip__in=options['ip']).only('id', 'ip', 'key', 'scan__id').all() 31 | print(scanners) 32 | self.parse_keys(scanners=scanners) 33 | 34 | if options['id'] is not None: 35 | scanners = Scanner.objects.filter(id__in=options['id']).only('id', 'ip', 'key', 'scan__id').all() 36 | self.parse_keys(scanners=scanners) 37 | 38 | if options['scan'] is not None: 39 | scanners = Scanner.objects.filter(scan_id__in=options['scan']).only('id', 'ip', 'key', 'scan__id').all() 40 | self.parse_keys(scanners=scanners) 41 | 42 | if options['scan'] is None and options['id'] is None and options['ip'] is None: 43 | self.stdout.write(self.style.ERROR("No keys exported because nothing was specified.")) 44 | else: 45 | self.stdout.write(self.style.SUCCESS("Successfully exported keys.")) 46 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.3 on 2016-04-21 17:20 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.conf import settings 7 | import django.core.validators 8 | from django.db import migrations, models 9 | import django.db.models.deletion 10 | import scanner.models 11 | import uuid 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | initial = True 17 | 18 | dependencies = [ 19 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 20 | ('auth', '0007_alter_validators_add_error_messages'), 21 | ] 22 | 23 | operations = [ 24 | migrations.CreateModel( 25 | name='Attachment', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('filename', models.FileField(upload_to=scanner.models.attachment_path)), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name='Job', 33 | fields=[ 34 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 35 | ('status', models.CharField(choices=[('P', 'Pending'), ('E', 'Executing'), ('R', 'Needs retry'), ('K', 'Killed'), ('C', 'Complete'), ('H', 'H')], default='P', editable=False, max_length=1)), 36 | ('attempts', models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)])), 37 | ('command', models.TextField()), 38 | ('target_hosts', models.TextField()), 39 | ], 40 | ), 41 | migrations.CreateModel( 42 | name='JobLog', 43 | fields=[ 44 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 45 | ('record_date', models.DateTimeField(auto_now_add=True)), 46 | ('start_time', models.DateTimeField(blank=True, null=True)), 47 | ('end_time', models.DateTimeField(blank=True, null=True)), 48 | ('attempt', models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)])), 49 | ('return_code', models.PositiveSmallIntegerField(blank=True, default=None, null=True)), 50 | ('stdout', models.TextField(blank=True, default='', null=True)), 51 | ('stderr', models.TextField(blank=True, default='', null=True)), 52 | ('job', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='raw_results', to='scanner.Job')), 53 | ], 54 | ), 55 | migrations.CreateModel( 56 | name='LogUser', 57 | fields=[ 58 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 59 | ('username', models.CharField(max_length=50)), 60 | ('ip', models.CharField(max_length=64)), 61 | ('dt', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='Date/Time')), 62 | ('action', models.CharField(max_length=15)), 63 | ], 64 | ), 65 | migrations.CreateModel( 66 | name='Profile', 67 | fields=[ 68 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 69 | ('name', models.CharField(max_length=150, unique=True)), 70 | ('command', models.TextField(default='', help_text='Please use for target ranges.', verbose_name='Command Line')), 71 | ('tool', models.CharField(choices=[('/usr/bin/nmap', 'nmap'), ('/opt/portplow/masscan/bin/masscan', 'masscan')], max_length=50)), 72 | ('description', models.TextField(null=True)), 73 | ], 74 | ), 75 | migrations.CreateModel( 76 | name='Scan', 77 | fields=[ 78 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 79 | ('name', models.CharField(default='', max_length=150)), 80 | ('hosts', models.TextField(help_text='One range/IP per line')), 81 | ('status', models.CharField(choices=[('P', 'Pending'), ('S', 'Setting up'), ('R', 'Running'), ('C', 'Complete'), ('H', 'On Hold')], default='P', max_length=1)), 82 | ('chunk_size', models.PositiveIntegerField(default=8, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(256)])), 83 | ('scanner_count', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])), 84 | ('deconfliction_message', models.TextField(blank=True, null=True)), 85 | ('htaccess', models.TextField(blank=True, null=True, verbose_name='Scanner Passwords')), 86 | ('scan_hours', models.TextField(blank=True, null=True)), 87 | ('start_date', models.DateTimeField(null=True)), 88 | ('stop_date', models.DateTimeField(null=True)), 89 | ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scans', to='auth.Group')), 90 | ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scans', to='scanner.Profile')), 91 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 92 | ], 93 | ), 94 | migrations.CreateModel( 95 | name='Scanner', 96 | fields=[ 97 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 98 | ('name', models.CharField(max_length=50)), 99 | ('notes', models.TextField(null=True)), 100 | ('status', models.CharField(choices=[('A', 'Active'), ('D', 'Decommissioned'), ('E', 'Error')], default='A', max_length=1)), 101 | ('ip', models.GenericIPAddressField(null=True)), 102 | ('key', models.TextField(editable=False, null=True)), 103 | ('external_id', models.CharField(max_length=40, null=True)), 104 | ('token', models.CharField(default=scanner.models.generate_random_password, max_length=50)), 105 | ('scan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='scanners', to='scanner.Scan')), 106 | ], 107 | ), 108 | migrations.CreateModel( 109 | name='ScannerLog', 110 | fields=[ 111 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 112 | ('ip', models.GenericIPAddressField(null=True)), 113 | ('dt', models.DateTimeField(default=datetime.datetime.utcnow, editable=False)), 114 | ('log_type', models.CharField(choices=[('C', 'Checkin'), ('A', 'Assigned job'), ('E', 'Error'), ('D', 'Data Received')], max_length=1)), 115 | ('content', models.TextField()), 116 | ('scanner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='scanner.Scanner')), 117 | ], 118 | ), 119 | migrations.CreateModel( 120 | name='ScanResult', 121 | fields=[ 122 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 123 | ('ip', models.CharField(max_length=15)), 124 | ('start_time', models.DateTimeField(blank=True, null=True)), 125 | ('end_time', models.DateTimeField(blank=True, null=True)), 126 | ('hostname', models.CharField(max_length=150)), 127 | ('port', models.SmallIntegerField(blank=True, null=True)), 128 | ('protocol', models.CharField(blank=True, max_length=10, null=True)), 129 | ('state', models.CharField(blank=True, max_length=20, null=True)), 130 | ('service', models.CharField(blank=True, max_length=40, null=True)), 131 | ('reason', models.CharField(blank=True, max_length=20, null=True)), 132 | ('joblog', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='parsed_results', to='scanner.JobLog')), 133 | ('scan', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='parsed_results', to='scanner.Scan')), 134 | ], 135 | ), 136 | migrations.AddField( 137 | model_name='joblog', 138 | name='scanner', 139 | field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.DO_NOTHING, to='scanner.Scanner'), 140 | ), 141 | migrations.AddField( 142 | model_name='job', 143 | name='profile', 144 | field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='jobs', to='scanner.Profile'), 145 | ), 146 | migrations.AddField( 147 | model_name='job', 148 | name='scan', 149 | field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='scanner.Scan'), 150 | ), 151 | migrations.AddField( 152 | model_name='job', 153 | name='scanner_lock', 154 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executing_jobs', to='scanner.Scanner'), 155 | ), 156 | migrations.AddField( 157 | model_name='attachment', 158 | name='job', 159 | field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='attachments', to='scanner.Job'), 160 | ), 161 | migrations.AddField( 162 | model_name='attachment', 163 | name='scan', 164 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='scanner.Scan'), 165 | ), 166 | migrations.AddField( 167 | model_name='attachment', 168 | name='scanner', 169 | field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='attachments', to='scanner.Scanner'), 170 | ), 171 | ] 172 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/0002_auto_20160421_1910.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.3 on 2016-04-21 19:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('scanner', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='scanner', 17 | name='name', 18 | field=models.CharField(max_length=100), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/0003_auto_20160421_1914.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.3 on 2016-04-21 19:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import scanner.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('scanner', '0002_auto_20160421_1910'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='scanner', 18 | name='token', 19 | field=models.CharField(default=scanner.models.generate_random_password, max_length=256), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/0004_auto_20160423_0208.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.3 on 2016-04-23 02:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import scanner.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('scanner', '0003_auto_20160421_1914'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='scanresult', 18 | name='mac', 19 | field=models.CharField(blank=True, max_length=20, null=True), 20 | ), 21 | migrations.AddField( 22 | model_name='scanresult', 23 | name='os', 24 | field=models.CharField(blank=True, max_length=50, null=True), 25 | ), 26 | migrations.AlterField( 27 | model_name='job', 28 | name='status', 29 | field=models.CharField(choices=[('P', 'Pending'), ('E', 'Executing'), ('R', 'Retry'), ('K', 'Killed'), ('C', 'Complete'), ('H', 'Hold')], default='P', editable=False, max_length=1), 30 | ), 31 | migrations.AlterField( 32 | model_name='scan', 33 | name='status', 34 | field=models.CharField(choices=[('P', 'Pending'), ('S', 'Setting up'), ('R', 'Running'), ('C', 'Complete'), ('H', 'On Hold')], default='R', max_length=1), 35 | ), 36 | migrations.AlterField( 37 | model_name='scanner', 38 | name='name', 39 | field=models.CharField(max_length=100), 40 | ), 41 | migrations.AlterField( 42 | model_name='scanner', 43 | name='token', 44 | field=models.CharField(default=scanner.models.generate_random_password, max_length=256), 45 | ), 46 | migrations.AlterField( 47 | model_name='scanresult', 48 | name='ip', 49 | field=models.CharField(blank=True, max_length=15, null=True), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/0005_joblog_parsed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.3 on 2016-04-23 03:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('scanner', '0004_auto_20160423_0208'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='joblog', 17 | name='parsed', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/0006_auto_20160505_1716.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.3 on 2016-05-05 17:16 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('scanner', '0005_joblog_parsed'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='attachment', 18 | name='scanner', 19 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='scanner.Scanner'), 20 | ), 21 | migrations.AlterField( 22 | model_name='joblog', 23 | name='scanner', 24 | field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='scanner.Scanner'), 25 | ), 26 | migrations.AlterField( 27 | model_name='scanner', 28 | name='scan', 29 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scanners', to='scanner.Scan'), 30 | ), 31 | migrations.AlterField( 32 | model_name='scanresult', 33 | name='joblog', 34 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parsed_results', to='scanner.JobLog'), 35 | ), 36 | migrations.AlterField( 37 | model_name='scanresult', 38 | name='scan', 39 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parsed_results', to='scanner.Scan'), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/0007_auto_20160513_0135.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.3 on 2016-05-13 01:35 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('scanner', '0006_auto_20160505_1716'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='scanner', 17 | name='external_id', 18 | field=models.CharField(max_length=100, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /portplow/scanner/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/scanner/migrations/__init__.py -------------------------------------------------------------------------------- /portplow/scanner/static/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * 1. Remove the gray background on active links in IE 10. 89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 90 | */ 91 | 92 | a { 93 | background-color: transparent; /* 1 */ 94 | -webkit-text-decoration-skip: objects; /* 2 */ 95 | } 96 | 97 | /** 98 | * Remove the outline on focused links when they are also active or hovered 99 | * in all browsers (opinionated). 100 | */ 101 | 102 | a:active, 103 | a:hover { 104 | outline-width: 0; 105 | } 106 | 107 | /* Text-level semantics 108 | ========================================================================== */ 109 | 110 | /** 111 | * 1. Remove the bottom border in Firefox 39-. 112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: none; /* 1 */ 117 | text-decoration: underline; /* 2 */ 118 | text-decoration: underline dotted; /* 2 */ 119 | } 120 | 121 | /** 122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: inherit; 128 | } 129 | 130 | /** 131 | * Add the correct font weight in Chrome, Edge, and Safari. 132 | */ 133 | 134 | b, 135 | strong { 136 | font-weight: bolder; 137 | } 138 | 139 | /** 140 | * Add the correct font style in Android 4.3-. 141 | */ 142 | 143 | dfn { 144 | font-style: italic; 145 | } 146 | 147 | /** 148 | * Correct the font size and margin on `h1` elements within `section` and 149 | * `article` contexts in Chrome, Firefox, and Safari. 150 | */ 151 | 152 | h1 { 153 | font-size: 2em; 154 | margin: 0.67em 0; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Remove the border on images inside links in IE 10-. 200 | */ 201 | 202 | img { 203 | border-style: none; 204 | } 205 | 206 | /** 207 | * Hide the overflow in IE. 208 | */ 209 | 210 | svg:not(:root) { 211 | overflow: hidden; 212 | } 213 | 214 | /* Grouping content 215 | ========================================================================== */ 216 | 217 | /** 218 | * 1. Correct the inheritance and scaling of font size in all browsers. 219 | * 2. Correct the odd `em` font sizing in all browsers. 220 | */ 221 | 222 | code, 223 | kbd, 224 | pre, 225 | samp { 226 | font-family: monospace, monospace; /* 1 */ 227 | font-size: 1em; /* 2 */ 228 | } 229 | 230 | /** 231 | * Add the correct margin in IE 8. 232 | */ 233 | 234 | figure { 235 | margin: 1em 40px; 236 | } 237 | 238 | /** 239 | * 1. Add the correct box sizing in Firefox. 240 | * 2. Show the overflow in Edge and IE. 241 | */ 242 | 243 | hr { 244 | box-sizing: content-box; /* 1 */ 245 | height: 0; /* 1 */ 246 | overflow: visible; /* 2 */ 247 | } 248 | 249 | /* Forms 250 | ========================================================================== */ 251 | 252 | /** 253 | * 1. Change font properties to `inherit` in all browsers (opinionated). 254 | * 2. Remove the margin in Firefox and Safari. 255 | */ 256 | 257 | button, 258 | input, 259 | select, 260 | textarea { 261 | font: inherit; /* 1 */ 262 | margin: 0; /* 2 */ 263 | } 264 | 265 | /** 266 | * Restore the font weight unset by the previous rule. 267 | */ 268 | 269 | optgroup { 270 | font-weight: bold; 271 | } 272 | 273 | /** 274 | * Show the overflow in IE. 275 | * 1. Show the overflow in Edge. 276 | */ 277 | 278 | button, 279 | input { /* 1 */ 280 | overflow: visible; 281 | } 282 | 283 | /** 284 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 285 | * 1. Remove the inheritance of text transform in Firefox. 286 | */ 287 | 288 | button, 289 | select { /* 1 */ 290 | text-transform: none; 291 | } 292 | 293 | /** 294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 295 | * controls in Android 4. 296 | * 2. Correct the inability to style clickable types in iOS and Safari. 297 | */ 298 | 299 | button, 300 | html [type="button"], /* 1 */ 301 | [type="reset"], 302 | [type="submit"] { 303 | -webkit-appearance: button; /* 2 */ 304 | } 305 | 306 | /** 307 | * Remove the inner border and padding in Firefox. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | [type="button"]::-moz-focus-inner, 312 | [type="reset"]::-moz-focus-inner, 313 | [type="submit"]::-moz-focus-inner { 314 | border-style: none; 315 | padding: 0; 316 | } 317 | 318 | /** 319 | * Restore the focus styles unset by the previous rule. 320 | */ 321 | 322 | button:-moz-focusring, 323 | [type="button"]:-moz-focusring, 324 | [type="reset"]:-moz-focusring, 325 | [type="submit"]:-moz-focusring { 326 | outline: 1px dotted ButtonText; 327 | } 328 | 329 | /** 330 | * Change the border, margin, and padding in all browsers (opinionated). 331 | */ 332 | 333 | fieldset { 334 | border: 1px solid #c0c0c0; 335 | margin: 0 2px; 336 | padding: 0.35em 0.625em 0.75em; 337 | } 338 | 339 | /** 340 | * 1. Correct the text wrapping in Edge and IE. 341 | * 2. Correct the color inheritance from `fieldset` elements in IE. 342 | * 3. Remove the padding so developers are not caught out when they zero out 343 | * `fieldset` elements in all browsers. 344 | */ 345 | 346 | legend { 347 | box-sizing: border-box; /* 1 */ 348 | color: inherit; /* 2 */ 349 | display: table; /* 1 */ 350 | max-width: 100%; /* 1 */ 351 | padding: 0; /* 3 */ 352 | white-space: normal; /* 1 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 404 | */ 405 | 406 | ::-webkit-input-placeholder { 407 | color: inherit; 408 | opacity: 0.54; 409 | } 410 | 411 | /** 412 | * 1. Correct the inability to style clickable types in iOS and Safari. 413 | * 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; /* 1 */ 418 | font: inherit; /* 2 */ 419 | } 420 | -------------------------------------------------------------------------------- /portplow/scanner/static/css/sticky-footer-navbar.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles 2 | -------------------------------------------------- */ 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | body { 8 | /* Margin bottom by footer height */ 9 | margin-bottom: 60px; 10 | } 11 | .footer { 12 | position: absolute; 13 | bottom: 0; 14 | width: 100%; 15 | /* Set the fixed height of the footer here */ 16 | height: 60px; 17 | background-color: #f5f5f5; 18 | } 19 | 20 | 21 | /* Custom page CSS 22 | -------------------------------------------------- */ 23 | /* Not required for template or sticky footer method. */ 24 | 25 | body > .container { 26 | padding: 60px 15px 0; 27 | } 28 | .container .text-muted { 29 | margin: 20px 0; 30 | } 31 | 32 | .footer > .container { 33 | padding-right: 15px; 34 | padding-left: 15px; 35 | } 36 | 37 | code { 38 | font-size: 80%; 39 | } 40 | -------------------------------------------------------------------------------- /portplow/scanner/static/images/minis_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/scanner/static/images/minis_logo.png -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker-master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/scanner/static/libs/bootstrap-daterangepicker-master.zip -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | npm-debug.log 4 | *.sublime-workspace 5 | *.sublime-project 6 | *.sw[onp] 7 | -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/README.md: -------------------------------------------------------------------------------- 1 | # Date Range Picker for Bootstrap 2 | 3 | ![Improvely.com](http://i.imgur.com/LbAMf3D.png) 4 | 5 | This date range picker component for Bootstrap creates a dropdown menu from which a user can 6 | select a range of dates. I created it while building the UI for [Improvely](http://www.improvely.com), 7 | which needed a way to select date ranges for reports. 8 | 9 | Features include limiting the selectable date range, localizable strings and date formats, 10 | a single date picker mode, optional time picker (for e.g. making appointments or reservations), 11 | and styles that match the default Bootstrap 3 theme. 12 | 13 | ## [Documentation and Live Usage Examples](http://www.daterangepicker.com) 14 | 15 | ## [See It In a Live Application](https://awio.iljmp.com/5/drpdemogh) 16 | 17 | ## License 18 | 19 | This code is made available under the same license as Bootstrap. Moment.js is included in this repository 20 | for convenience. It is available under the [MIT license](http://www.opensource.org/licenses/mit-license.php). 21 | 22 | -- 23 | 24 | The MIT License (MIT) 25 | 26 | Copyright (c) 2012-2016 Dan Grossman 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in 36 | all copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 44 | THE SOFTWARE. 45 | -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-daterangepicker", 3 | "main": [ 4 | "daterangepicker.js", 5 | "daterangepicker.css" 6 | ], 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "test", 12 | "tests", 13 | "moment.js", 14 | "moment.min.js" 15 | ], 16 | "dependencies": { 17 | "jquery": ">=1.10", 18 | "moment": ">=2.9.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/daterangepicker.css: -------------------------------------------------------------------------------- 1 | .daterangepicker { 2 | position: absolute; 3 | color: inherit; 4 | background: #fff; 5 | border-radius: 4px; 6 | width: 278px; 7 | padding: 4px; 8 | margin-top: 1px; 9 | top: 100px; 10 | left: 20px; 11 | /* Calendars */ } 12 | .daterangepicker:before, .daterangepicker:after { 13 | position: absolute; 14 | display: inline-block; 15 | border-bottom-color: rgba(0, 0, 0, 0.2); 16 | content: ''; } 17 | .daterangepicker:before { 18 | top: -7px; 19 | border-right: 7px solid transparent; 20 | border-left: 7px solid transparent; 21 | border-bottom: 7px solid #ccc; } 22 | .daterangepicker:after { 23 | top: -6px; 24 | border-right: 6px solid transparent; 25 | border-bottom: 6px solid #fff; 26 | border-left: 6px solid transparent; } 27 | .daterangepicker.opensleft:before { 28 | right: 9px; } 29 | .daterangepicker.opensleft:after { 30 | right: 10px; } 31 | .daterangepicker.openscenter:before { 32 | left: 0; 33 | right: 0; 34 | width: 0; 35 | margin-left: auto; 36 | margin-right: auto; } 37 | .daterangepicker.openscenter:after { 38 | left: 0; 39 | right: 0; 40 | width: 0; 41 | margin-left: auto; 42 | margin-right: auto; } 43 | .daterangepicker.opensright:before { 44 | left: 9px; } 45 | .daterangepicker.opensright:after { 46 | left: 10px; } 47 | .daterangepicker.dropup { 48 | margin-top: -5px; } 49 | .daterangepicker.dropup:before { 50 | top: initial; 51 | bottom: -7px; 52 | border-bottom: initial; 53 | border-top: 7px solid #ccc; } 54 | .daterangepicker.dropup:after { 55 | top: initial; 56 | bottom: -6px; 57 | border-bottom: initial; 58 | border-top: 6px solid #fff; } 59 | .daterangepicker.dropdown-menu { 60 | max-width: none; 61 | z-index: 3001; } 62 | .daterangepicker.single .ranges, .daterangepicker.single .calendar { 63 | float: none; } 64 | .daterangepicker.show-calendar .calendar { 65 | display: block; } 66 | .daterangepicker .calendar { 67 | display: none; 68 | max-width: 270px; 69 | margin: 4px; } 70 | .daterangepicker .calendar.single .calendar-table { 71 | border: none; } 72 | .daterangepicker .calendar th, .daterangepicker .calendar td { 73 | white-space: nowrap; 74 | text-align: center; 75 | min-width: 32px; } 76 | .daterangepicker .calendar-table { 77 | border: 1px solid #fff; 78 | padding: 4px; 79 | border-radius: 4px; 80 | background: #fff; } 81 | .daterangepicker table { 82 | width: 100%; 83 | margin: 0; } 84 | .daterangepicker td, .daterangepicker th { 85 | text-align: center; 86 | width: 20px; 87 | height: 20px; 88 | border-radius: 4px; 89 | border: 1px solid transparent; 90 | white-space: nowrap; 91 | cursor: pointer; } 92 | .daterangepicker td.available:hover, .daterangepicker th.available:hover { 93 | background: #eee; } 94 | .daterangepicker td.week, .daterangepicker th.week { 95 | font-size: 80%; 96 | color: #ccc; } 97 | .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { 98 | background-color: #fff; 99 | border-color: transparent; 100 | color: #999; } 101 | .daterangepicker td.in-range { 102 | background-color: #ebf4f8; 103 | border-color: transparent; 104 | color: #000; 105 | border-radius: 0; } 106 | .daterangepicker td.start-date { 107 | border-radius: 4px 0 0 4px; } 108 | .daterangepicker td.end-date { 109 | border-radius: 0 4px 4px 0; } 110 | .daterangepicker td.start-date.end-date { 111 | border-radius: 4px; } 112 | .daterangepicker td.active, .daterangepicker td.active:hover { 113 | background-color: #357ebd; 114 | border-color: transparent; 115 | color: #fff; } 116 | .daterangepicker th.month { 117 | width: auto; } 118 | .daterangepicker td.disabled, .daterangepicker option.disabled { 119 | color: #999; 120 | cursor: not-allowed; 121 | text-decoration: line-through; } 122 | .daterangepicker select.monthselect, .daterangepicker select.yearselect { 123 | font-size: 12px; 124 | padding: 1px; 125 | height: auto; 126 | margin: 0; 127 | cursor: default; } 128 | .daterangepicker select.monthselect { 129 | margin-right: 2%; 130 | width: 56%; } 131 | .daterangepicker select.yearselect { 132 | width: 40%; } 133 | .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { 134 | width: 50px; 135 | margin-bottom: 0; } 136 | .daterangepicker .input-mini { 137 | border: 1px solid #ccc; 138 | border-radius: 4px; 139 | color: #555; 140 | height: 30px; 141 | line-height: 30px; 142 | display: block; 143 | vertical-align: middle; 144 | margin: 0 0 5px 0; 145 | padding: 0 6px 0 28px; 146 | width: 100%; } 147 | .daterangepicker .input-mini.active { 148 | border: 1px solid #08c; 149 | border-radius: 4px; } 150 | .daterangepicker .daterangepicker_input { 151 | position: relative; } 152 | .daterangepicker .daterangepicker_input i { 153 | position: absolute; 154 | left: 8px; 155 | top: 8px; } 156 | .daterangepicker .calendar-time { 157 | text-align: center; 158 | margin: 5px auto; 159 | line-height: 30px; 160 | position: relative; 161 | padding-left: 28px; } 162 | .daterangepicker .calendar-time select.disabled { 163 | color: #ccc; 164 | cursor: not-allowed; } 165 | 166 | .ranges { 167 | font-size: 11px; 168 | float: none; 169 | margin: 4px; 170 | text-align: left; } 171 | .ranges ul { 172 | list-style: none; 173 | margin: 0 auto; 174 | padding: 0; 175 | width: 100%; } 176 | .ranges li { 177 | font-size: 13px; 178 | background: #f5f5f5; 179 | border: 1px solid #f5f5f5; 180 | border-radius: 4px; 181 | color: #08c; 182 | padding: 3px 12px; 183 | margin-bottom: 8px; 184 | cursor: pointer; } 185 | .ranges li:hover { 186 | background: #08c; 187 | border: 1px solid #08c; 188 | color: #fff; } 189 | .ranges li.active { 190 | background: #08c; 191 | border: 1px solid #08c; 192 | color: #fff; } 193 | 194 | /* Larger Screen Styling */ 195 | @media (min-width: 564px) { 196 | .daterangepicker { 197 | width: auto; } 198 | .daterangepicker .ranges ul { 199 | width: 160px; } 200 | .daterangepicker.single .ranges ul { 201 | width: 100%; } 202 | .daterangepicker.single .calendar.left { 203 | clear: none; } 204 | .daterangepicker.single .ranges, .daterangepicker.single .calendar { 205 | float: left; } 206 | .daterangepicker .calendar.left { 207 | clear: left; 208 | margin-right: 0; } 209 | .daterangepicker .calendar.left .calendar-table { 210 | border-right: none; 211 | border-top-right-radius: 0; 212 | border-bottom-right-radius: 0; } 213 | .daterangepicker .calendar.right { 214 | margin-left: 0; } 215 | .daterangepicker .calendar.right .calendar-table { 216 | border-left: none; 217 | border-top-left-radius: 0; 218 | border-bottom-left-radius: 0; } 219 | .daterangepicker .left .daterangepicker_input { 220 | padding-right: 12px; } 221 | .daterangepicker .calendar.left .calendar-table { 222 | padding-right: 12px; } 223 | .daterangepicker .ranges, .daterangepicker .calendar { 224 | float: left; } } 225 | 226 | @media (min-width: 730px) { 227 | .daterangepicker .ranges { 228 | width: auto; 229 | float: left; } 230 | .daterangepicker .calendar.left { 231 | clear: none; } } 232 | 233 | -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A date range picker for Bootstrap 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 |
22 | 23 |

Configuration Builder

24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 | 52 |
53 | 54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 | 64 |
65 | 66 |
67 | 70 |
71 | 72 |
73 | 76 |
77 | 78 |
79 | 82 |
83 | 84 |
85 | 88 |
89 | 90 |
91 | 94 |
95 | 96 |
97 | 100 |
101 | 102 |
103 | 104 | 105 |
106 | 107 |
108 | 111 |
112 | 113 |
114 | 117 |
118 | 119 |
120 | 123 |
124 | 125 |
126 | 129 |
130 | 131 |
132 | 135 |
136 | 137 |
138 | 141 |
142 | 143 |
144 | 147 |
148 | 149 |
150 |
151 | 152 |
153 | 154 | 159 |
160 | 161 |
162 | 163 | 167 |
168 | 169 |
170 | 171 | 172 |
173 | 174 |
175 | 176 | 177 |
178 | 179 |
180 | 181 | 182 |
183 | 184 |
185 | 186 |
187 |
188 | 189 |
190 | 191 |
192 | 193 |
194 |

Your Date Range Picker

195 | 196 | 197 |
198 | 199 |
200 |

Configuration

201 | 202 |
203 | 204 |
205 |
206 | 207 |
208 | 209 |
210 | 211 | 217 | 218 | 351 | 352 | 353 | 354 | -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/drp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/scanner/static/libs/bootstrap-daterangepicker/drp.png -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'dangrossman:bootstrap-daterangepicker', 3 | version: '2.1.19', 4 | summary: 'Date range picker component for Bootstrap', 5 | git: 'https://github.com/dangrossman/bootstrap-daterangepicker', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('METEOR@0.9.0.1'); 11 | 12 | api.use('twbs:bootstrap@3.3.4', ["client"], {weak: true}); 13 | api.use('momentjs:moment@2.10.3', ["client"]); 14 | api.use('jquery@1.11.3_2', ["client"]); 15 | 16 | api.addFiles('daterangepicker.js', ["client"]); 17 | api.addFiles('daterangepicker.css', ["client"]); 18 | }); 19 | -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-daterangepicker", 3 | "version": "2.1.19", 4 | "description": "Date range picker component for Bootstrap", 5 | "main": "daterangepicker.js", 6 | "style": "daterangepicker.css", 7 | "scripts": { 8 | "scss": "node-sass daterangepicker.scss > daterangepicker.css", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/dangrossman/bootstrap-daterangepicker.git" 14 | }, 15 | "author": { 16 | "name": "Dan Grossman", 17 | "email": "dan@dangrossman.info", 18 | "url": "http://www.dangrossman.info" 19 | }, 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/dangrossman/bootstrap-daterangepicker/issues" 23 | }, 24 | "homepage": "https://github.com/dangrossman/bootstrap-daterangepicker", 25 | "dependencies": { 26 | "jquery": ">=1.10", 27 | "moment": "^2.9.0" 28 | }, 29 | "devDependencies": { 30 | "node-sass": "^3.4.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/website/website.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 15px; 3 | line-height: 1.6em; 4 | position: relative; 5 | margin: 0; 6 | } 7 | .container { 8 | width: 95%; 9 | max-width: 1260px; 10 | } 11 | p, pre { 12 | margin-bottom: 2em; 13 | } 14 | .main h2 { 15 | font-weight: bold; 16 | margin: 60px 0 20px 0; 17 | } 18 | .main h3 { 19 | margin: 60px 0 20px 0; 20 | } 21 | .main h4 { 22 | margin: 0 0 10px 0; 23 | font-weight: bold; 24 | } 25 | ul.nobullets { 26 | margin: 0; 27 | padding: 0; 28 | list-style-position: inside; 29 | } 30 | li { 31 | padding-bottom: 1em; 32 | } 33 | #sidebar { 34 | top: 20px; 35 | width: 336px; 36 | } 37 | #sidebar ul { 38 | margin-bottom: 5px; 39 | } 40 | #sidebar li { 41 | margin-bottom: 0; 42 | padding-bottom: 0; 43 | } 44 | #sidebar li ul { 45 | display: none; 46 | } 47 | #sidebar li.active ul { 48 | display: block; 49 | } 50 | #sidebar li li { 51 | padding: 4px 0; 52 | } 53 | input[type="text"] { 54 | padding: 6px; 55 | width: 100%; 56 | border-radius: 4px; 57 | } 58 | .navbar { 59 | text-align: left; 60 | margin: 0; 61 | border: 0; 62 | } 63 | .navbar-inverse { 64 | background: #222; 65 | } 66 | .navbar .container { 67 | padding: 0 20px; 68 | } 69 | .navbar-nav li a:link, .navbar-nav li a:visited { 70 | font-weight: bold; 71 | color: #fff; 72 | font-size: 16px; 73 | } 74 | .navbar-nav li { 75 | background: #fff; 76 | } 77 | .navbar-nav li a:hover { 78 | opacity: 0.8; 79 | } 80 | .navbar-nav li { 81 | padding: 0; 82 | } 83 | .navbar-inverse .navbar-text { 84 | margin: 18px 0 0 0; 85 | color: #eee; 86 | } 87 | #footer { 88 | background: #222; 89 | margin-top: 80px; 90 | padding: 30px; 91 | color: #fff; 92 | text-align: center; 93 | } 94 | #footer a:link, #footer a:visited { 95 | color: #fff; 96 | border-bottom: 1px dotted #fff; 97 | } 98 | #jumbo { 99 | background: #f5f5f5 linear-gradient(to bottom,#eee 0,#f5f5f5 100%); 100 | color: #000; 101 | padding: 30px 0; 102 | margin-bottom: 30px; 103 | } 104 | #jumbo .btn { 105 | border-radius: 0; 106 | } 107 | #config .demo { position: relative; } 108 | #config .demo i { position: absolute; bottom: 10px; right: 24px; top: auto; cursor: pointer; } 109 | 110 | #rightcol { 111 | margin-left: 366px; 112 | } 113 | 114 | #nav-spy { 115 | float: left; 116 | width: 300px; 117 | } 118 | 119 | @media (max-width: 980px) { 120 | #rightcol { 121 | margin-left: 0; 122 | } 123 | #nav-spy { 124 | float: none; 125 | position: relative; 126 | } 127 | } -------------------------------------------------------------------------------- /portplow/scanner/static/libs/bootstrap-daterangepicker/website/website.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | $('#config-text').keyup(function() { 4 | eval($(this).val()); 5 | }); 6 | 7 | $('.configurator input, .configurator select').change(function() { 8 | updateConfig(); 9 | }); 10 | 11 | $('.demo i').click(function() { 12 | $(this).parent().find('input').click(); 13 | }); 14 | 15 | $('#startDate').daterangepicker({ 16 | singleDatePicker: true, 17 | startDate: moment().subtract(6, 'days') 18 | }); 19 | 20 | $('#endDate').daterangepicker({ 21 | singleDatePicker: true, 22 | startDate: moment() 23 | }); 24 | 25 | updateConfig(); 26 | 27 | function updateConfig() { 28 | var options = {}; 29 | 30 | if ($('#singleDatePicker').is(':checked')) 31 | options.singleDatePicker = true; 32 | 33 | if ($('#showDropdowns').is(':checked')) 34 | options.showDropdowns = true; 35 | 36 | if ($('#showWeekNumbers').is(':checked')) 37 | options.showWeekNumbers = true; 38 | 39 | if ($('#showISOWeekNumbers').is(':checked')) 40 | options.showISOWeekNumbers = true; 41 | 42 | if ($('#timePicker').is(':checked')) 43 | options.timePicker = true; 44 | 45 | if ($('#timePicker24Hour').is(':checked')) 46 | options.timePicker24Hour = true; 47 | 48 | if ($('#timePickerIncrement').val().length && $('#timePickerIncrement').val() != 1) 49 | options.timePickerIncrement = parseInt($('#timePickerIncrement').val(), 10); 50 | 51 | if ($('#timePickerSeconds').is(':checked')) 52 | options.timePickerSeconds = true; 53 | 54 | if ($('#autoApply').is(':checked')) 55 | options.autoApply = true; 56 | 57 | if ($('#dateLimit').is(':checked')) 58 | options.dateLimit = { days: 7 }; 59 | 60 | if ($('#ranges').is(':checked')) { 61 | options.ranges = { 62 | 'Today': [moment(), moment()], 63 | 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], 64 | 'Last 7 Days': [moment().subtract(6, 'days'), moment()], 65 | 'Last 30 Days': [moment().subtract(29, 'days'), moment()], 66 | 'This Month': [moment().startOf('month'), moment().endOf('month')], 67 | 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] 68 | }; 69 | } 70 | 71 | if ($('#locale').is(':checked')) { 72 | options.locale = { 73 | format: 'MM/DD/YYYY', 74 | separator: ' - ', 75 | applyLabel: 'Apply', 76 | cancelLabel: 'Cancel', 77 | fromLabel: 'From', 78 | toLabel: 'To', 79 | customRangeLabel: 'Custom', 80 | daysOfWeek: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr','Sa'], 81 | monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 82 | firstDay: 1 83 | }; 84 | } 85 | 86 | if (!$('#linkedCalendars').is(':checked')) 87 | options.linkedCalendars = false; 88 | 89 | if (!$('#autoUpdateInput').is(':checked')) 90 | options.autoUpdateInput = false; 91 | 92 | if ($('#alwaysShowCalendars').is(':checked')) 93 | options.alwaysShowCalendars = true; 94 | 95 | if ($('#parentEl').val().length) 96 | options.parentEl = $('#parentEl').val(); 97 | 98 | if ($('#startDate').val().length) 99 | options.startDate = $('#startDate').val(); 100 | 101 | if ($('#endDate').val().length) 102 | options.endDate = $('#endDate').val(); 103 | 104 | if ($('#minDate').val().length) 105 | options.minDate = $('#minDate').val(); 106 | 107 | if ($('#maxDate').val().length) 108 | options.maxDate = $('#maxDate').val(); 109 | 110 | if ($('#opens').val().length && $('#opens').val() != 'right') 111 | options.opens = $('#opens').val(); 112 | 113 | if ($('#drops').val().length && $('#drops').val() != 'down') 114 | options.drops = $('#drops').val(); 115 | 116 | if ($('#buttonClasses').val().length && $('#buttonClasses').val() != 'btn btn-sm') 117 | options.buttonClasses = $('#buttonClasses').val(); 118 | 119 | if ($('#applyClass').val().length && $('#applyClass').val() != 'btn-success') 120 | options.applyClass = $('#applyClass').val(); 121 | 122 | if ($('#cancelClass').val().length && $('#cancelClass').val() != 'btn-default') 123 | options.cancelClass = $('#cancelClass').val(); 124 | 125 | $('#config-text').val("$('#demo').daterangepicker(" + JSON.stringify(options, null, ' ') + ", function(start, end, label) {\n console.log(\"New date range selected: ' + start.format('YYYY-MM-DD') + ' to ' + end.format('YYYY-MM-DD') + ' (predefined range: ' + label + ')\");\n});"); 126 | 127 | $('#config-demo').daterangepicker(options, function(start, end, label) { console.log('New date range selected: ' + start.format('YYYY-MM-DD') + ' to ' + end.format('YYYY-MM-DD') + ' (predefined range: ' + label + ')'); }); 128 | 129 | } 130 | 131 | if ($(window).width() > 980) { 132 | $('#sidebar').affix({ 133 | offset: { 134 | top: 300, 135 | bottom: function () { 136 | return (this.bottom = $('.footer').outerHeight(true)) 137 | } 138 | } 139 | }); 140 | } 141 | $('body').scrollspy({ target: '#nav-spy', offset: 20 }); 142 | }); -------------------------------------------------------------------------------- /portplow/scanner/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from datetime import datetime 3 | from celery import shared_task 4 | from ipaddress import ip_address, ip_network 5 | import logging 6 | from random import shuffle 7 | from django.core.cache import cache 8 | # from scanner.models import JobLog, Scan, Job, ScannerLog, Scanner 9 | # from scanner.signals import cleanup_scan, ExternalScanner 10 | 11 | 12 | @shared_task 13 | def parse_results(scan_id): 14 | """ 15 | Parse results of a scan. 16 | """ 17 | from scanner.models import Scan, JobLog 18 | from scanner.signals import parse_nmap_results 19 | scan = Scan.objects.get(id=scan_id) 20 | not_parsed = JobLog.objects.filter(job__scan=scan, 21 | parsed=False, 22 | job__profile__tool="/usr/bin/nmap").all() 23 | if not_parsed.count() > 0: 24 | for rec in not_parsed: 25 | parse_nmap_results(joblog=rec) 26 | rec.parsed = True 27 | rec.save() 28 | return "Parsed results for {}".format(scan.name) 29 | else: 30 | return "No results to parse." 31 | 32 | 33 | @shared_task 34 | def update_progress_counts(): 35 | from scanner.models import Scan 36 | scans = Scan.objects.all() 37 | for scan in scans: 38 | key_id = "_scanner.progress.{}".format(str(scan.id)) 39 | cache.delete(key_id) 40 | print("Scan {}: {}".format(scan.id, scan.progress())) 41 | return "Counts have been updated." 42 | 43 | 44 | @shared_task 45 | def cleanup_completed_scans(): 46 | from scanner.models import Scan 47 | from scanner.signals import cleanup_scan 48 | scans = Scan.objects.filter(status=Scan.RUNNING).all() 49 | for scan in scans: 50 | if scan.is_complete(): 51 | print("Scan {} is complete. Issuing cleanup command.".format(scan.name)) 52 | cleanup_scan(scan) 53 | return "Finished cleaning up completed scans." 54 | 55 | 56 | @shared_task 57 | def add_scanner_log(scanner_id=None, log_type=None, content=None, scanner_ip=None): 58 | ''' 59 | Update the last seen in the cache if it's a checkin. 60 | Otherwise, add it to the database. 61 | ''' 62 | from scanner.models import ScannerLog 63 | if log_type == ScannerLog.CHECKIN: 64 | key_id = "_scanner.last_seen.{}".format(str(scanner_id)) 65 | cache.set(key_id, datetime.utcnow().timestamp(), timeout=None) 66 | return "Checkin from {}".format(scanner_id) 67 | else: 68 | log_entry = ScannerLog(scanner_id=scanner_id, 69 | log_type=log_type, 70 | ip=scanner_ip, 71 | content=content) 72 | log_entry.save() 73 | return "Log from {} ({}) saved - {}.".format(scanner_id, scanner_ip, log_type) 74 | 75 | 76 | @shared_task 77 | def add_scanner_ip(scanner_id=None, new_ip=None): 78 | from scanner.models import Scanner 79 | scanner = Scanner.objects.filter(id=scanner_id).update(ip=new_ip) 80 | return "Updated IP for {} to {} -- {}".format(scanner_id, new_ip, scanner) 81 | 82 | 83 | @shared_task 84 | def complete_scan_setup(scan_id): 85 | from scanner.models import Scan, Job, Scanner 86 | from scanner.signals import ExternalScanner 87 | scan = Scan.objects.get(id=scan_id) 88 | log = logging.getLogger(__name__) 89 | # Create a list of IPs 90 | ips = [] 91 | networks = scan.hosts.split("\n") 92 | for network in networks: 93 | try: 94 | ips.extend(list(map(str, ip_network(network.strip(), strict=False)))) 95 | except ValueError as e: 96 | log.error("Error: {}".format(str(e))) 97 | pass 98 | 99 | if len(ips) == 0: 100 | return 101 | 102 | shuffle(ips) 103 | 104 | job_list = [] 105 | for x in range(0, len(ips), scan.chunk_size): 106 | ip_list = ips[x:x + scan.chunk_size] 107 | command_line = scan.profile.command.replace("", " ".join(ip_list)) 108 | job = Job(scan=scan, command=command_line, profile=scan.profile, target_hosts=",".join(ip_list)) 109 | job_list.append(job) 110 | if len(job_list) == 1000: 111 | Job.objects.bulk_create(job_list) 112 | job_list = [] 113 | 114 | if len(job_list) > 0: 115 | Job.objects.bulk_create(job_list) 116 | 117 | records = [] 118 | # Create each of the scanners 119 | for x in range(0, scan.scanner_count): 120 | log.debug("Creating scanner.") 121 | scanner_record = Scanner(scan=scan) 122 | scanner = ExternalScanner(scan=scan, scanner=scanner_record, count=x) 123 | scanner.setup() 124 | scanner_record.external_id = scanner.node.get_uuid() 125 | scanner_record.key = scanner.key.exportKey().decode('utf8') 126 | scanner_record.save() 127 | records.append(scanner_record) 128 | 129 | scan.status = Scan.RUNNING 130 | scan.save() 131 | 132 | return "Scan setup for \"{}\" has been completed.".format(scan.name) 133 | 134 | 135 | @shared_task 136 | def load_job_queues(): 137 | from scanner.models import Scan, Job 138 | from django_redis import get_redis_connection 139 | 140 | scans = Scan.objects.filter(status=Scan.RUNNING).all() 141 | raw_con = get_redis_connection("default") 142 | 143 | for scan in scans: 144 | key_id = "__scanner.job_queue.{}".format(scan.id) 145 | key_id_executing = "__scanner.executing.*" 146 | executing_jobs = [x.decode('utf-8').replace(":1:__scanner.job.", "") for x in raw_con.lrange(key_id_executing, 0, -1)] 147 | # Get the number of cached jobs 148 | num_cached = raw_con.llen(key_id) 149 | loaded_ids = [x.decode('utf-8') for x in raw_con.lrange(key_id, 0, -1)] 150 | max_cached = 100 151 | if num_cached < max_cached: 152 | jobs = scan.jobs.\ 153 | filter(status__in=[Job.PENDING, Job.RETRY]).\ 154 | exclude(id__in=loaded_ids).exclude(id__in=executing_jobs).\ 155 | only("id", "command", "profile__tool", "attempts")[:max_cached - num_cached] 156 | job_count = jobs.count() 157 | for job in jobs: 158 | raw_con.rpush(key_id, str(job.id)) 159 | job_key = "__scanner.job.{}".format(str(job.id)) 160 | cache.set(job_key, job, timeout=None) 161 | print("Added job {} to cache.".format(job_key)) 162 | print("Loaded {} jobs on the queue for {}".format(job_count, scan.name)) 163 | else: 164 | print("Job queue for {} is already maxed out.".format(scan.name)) 165 | 166 | return "Completed load_jobs_queues" 167 | 168 | 169 | @shared_task 170 | def assign_job(job_id, scanner_id): 171 | from scanner.models import Scanner, Job 172 | scanner = Scanner.objects.get(id=scanner_id) 173 | job = Job.objects.get(id=job_id) 174 | job.status = Job.EXECUTING 175 | job.scanner_lock = scanner 176 | job.save() 177 | return "Assigned job {} to scanner {}".format(str(job.id), scanner.ip) 178 | 179 | 180 | @shared_task 181 | def clear_job_queues(): 182 | from scanner.models import Scan, Job 183 | from django_redis import get_redis_connection 184 | 185 | scans = Scan.objects.filter(status=Scan.RUNNING).all() 186 | raw_con = get_redis_connection("default") 187 | 188 | for scan in scans: 189 | key_id = "__scanner.job_queue.{}".format(str(scan.id)) 190 | raw_con.ltrim(key_id, 1, 0) 191 | 192 | @shared_task 193 | def update_completion_dates(): 194 | from scanner.models import Scan, Job 195 | 196 | scans = Scan.objects.filter(status=Scan.RUNNING).all() 197 | for scan in scans: 198 | key_id = "__scanner.complete.{}".format(str(scan.id)) 199 | cache.set(key_id, scan.estimate_remaining(), timeout=None) 200 | 201 | return "Updated scan completion estimates." 202 | 203 | 204 | @shared_task 205 | def export_scan(scan_id): 206 | from scanner.models import JobLog, Job, Scan 207 | from datetime import datetime 208 | from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 209 | from django.core.paginator import Paginator 210 | 211 | import os 212 | 213 | try: 214 | scan = Scan.objects.get(id=scan_id) 215 | except Scan.DoesNotExist: 216 | return "Unable to export scan {}. Scan was not found.".format(scan_id) 217 | 218 | zip_name = "/opt/portplow/backups/{} - Scan Export {}.zip". \ 219 | format(datetime.now().strftime("%Y-%m-%d_%H.%M.%S"), scan.name) 220 | zip = ZipFile(zip_name, 'w', ZIP_DEFLATED, allowZip64=True) 221 | 222 | paginator = Paginator( 223 | JobLog.objects.filter(return_code=0, 224 | job_id__in=Job.objects.filter(scan=scan)).only('start_time', 'id', 225 | 'job__target_hosts', 226 | 'stdout').order_by('id').all(), 5000) 227 | 228 | for page in range(1, paginator.num_pages + 1): 229 | for result in paginator.page(page).object_list: 230 | if result.start_time is not None: 231 | dt = result.start_time.timetuple() 232 | folder_dt = result.start_time.strftime("%Y-%m-%d") 233 | else: 234 | dt = datetime.utcnow().timetuple() 235 | folder_dt = "unparsed" 236 | # output_path = os.path.join("/", "tmp", "scans", folder_dt) 237 | # if not os.path.exists(output_path): 238 | # print(output_path) 239 | # os.makedirs(output_path, exist_ok=True) 240 | # with open(os.path.join(output_path, str(result.id)), "w") as ft: 241 | # a = ft.write(result.stdout) 242 | info = ZipInfo("scans/{}/{}".format(folder_dt, str(result.id)), 243 | date_time=dt) 244 | info.compress_type = ZIP_DEFLATED 245 | info.comment = bytes(result.job.target_hosts, 'utf-8') 246 | info.create_system = 0 247 | zip.writestr(info, result.stdout) # 'utf-8')) 248 | 249 | zip.close() 250 | return "Created export for {} at {}".format(scan.name, zip_name) 251 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | PortPlow - {% block title %}Administration{% endblock title %} 7 | 8 | 9 | 10 | 12 | 13 | 14 | {% comment %} 15 | 17 | {% endcomment %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 68 | {% block head %} 69 | {% endblock head %} 70 | 71 | 72 | 117 |
118 | 119 | {% block breadcrumbs %} 120 | {% comment %} {% endcomment %} 125 | {% endblock breadcrumbs %} 126 | 127 | {% block messages %} 128 | 129 | {% if messages %} 130 | {% for message in messages %} 131 | 132 | 136 | {% endfor %} 137 | {% endif %} 138 | {% endblock %} 139 | {% block content %} 140 | {% endblock content %} 141 | 142 |
143 | 144 |
145 |
146 |

147 | PortPlow is a MINIS, LLC Project.
148 | Logged in as {{ request.user.username }}. (Logout) 149 |

150 |
151 |
152 | 153 | 154 | 155 | {% include 'session_security/all.html' %} 156 | 159 | 160 | 161 | {% block tail_js %} 162 | {% endblock %} 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/base_no-header.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | PortPlow - {% block title %}Administration{% endblock title %} 7 | 8 | 10 | 11 | 12 | 14 | 15 | 20 | {% block head %} 21 | {% endblock head %} 22 | 23 | 24 |
25 | 26 | {% block messages %} 27 | 28 | {% if messages %} 29 | {% for message in messages %} 30 | 34 | {% endfor %} 35 | {% endif %} 36 | {% endblock %} 37 | 38 | {% block content %} 39 | {% endblock content %} 40 | 41 |
42 | 43 | 44 | 45 | 48 | 49 | {% block tail_js %} 50 | {% endblock %} 51 | 52 | 53 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/base_plain.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | PortPlow - {% block title %}Administration{% endblock title %} 7 | 8 | 10 | 11 | 12 | 14 | 15 | 20 | {% block head %} 21 | {% endblock head %} 22 | 23 | 24 | 31 |
32 | 33 | {% block messages %} 34 | 35 | {% if messages %} 36 | {% for message in messages %} 37 | 41 | {% endfor %} 42 | {% endif %} 43 | {% endblock %} 44 | {% block content %} 45 | {% endblock content %} 46 | 47 |
48 | 49 | 50 | 51 | 54 | 55 | {% block tail_js %} 56 | {% endblock %} 57 | 58 | 59 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/group-list.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %}Profiles{% endblock title %} 5 | 6 | {% block breadcrumbs %} 7 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |

Groups {% comment %}{% if request.user.is_superuser %}New Profile{% endif %}{% endcomment %}

17 |
18 |
19 | {% if profiles.count == 0 %} 20 | No profiles are listed in the database. 21 | {% else %} 22 | 23 | 24 | 25 | 26 | {% comment %}{% endcomment %} 27 | 28 | 29 | 30 | {% for group in groups %} 31 | 32 | 33 | {% comment %}{% endcomment %} 34 | 35 | {% endfor %} 36 | 37 |
Name# of Users
{{ group.name }}{{ group.users.count }}
38 | {% endif %} 39 |
40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/login.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base_no-header.html" %} 2 | {% block title %}PortPlow.io - Login{% endblock %} 3 | 4 | {% block content %} 5 | {#

Sign in to your Account

#} 6 | 7 |
8 | 9 | 10 |
11 | {% csrf_token %} 12 | {% if next %} 13 | 14 | {% endif %} 15 | {% if form.errors %} 16 |
17 |

Your username and password didn't match. Please try again.

18 |
19 | {% endif %} 20 |
21 | 22 | {% comment %}{% endcomment %} 23 |
24 | 25 |
26 | 27 | {% comment %}{% endcomment %} 28 |
29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 | 37 | {% comment %}{% endcomment %} 43 |
44 | {% endblock %} 45 | 46 | {% block tail_js %} 47 | 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/profile-create.html: -------------------------------------------------------------------------------- 1 | {% extends 'scanner/base.html' %} 2 | {% load staticfiles %} 3 | {% load bootstrap3 %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block title %}Add Profile{% endblock title %} 7 | 8 | 9 | {% block header %} 10 | {% endblock %} 11 | 12 | {% block breadcrumbs %} 13 | 18 | {% endblock breadcrumbs %} 19 | 20 | {% block content %} 21 | 24 | 25 | {% crispy form %} 26 | 27 | {% endblock %} 28 | 29 | {% block tail_js %} 30 | 31 | {% comment %} {% endcomment %} 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/profile-list.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %}Profiles{% endblock title %} 5 | 6 | {% block breadcrumbs %} 7 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |

Profiles {% if request.user.is_superuser %}New Profile{% endif %}

17 |
18 |
19 | {% if profiles.count == 0 %} 20 | No profiles are listed in the database. 21 | {% else %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {# #} 30 | 31 | 32 | 33 | {% for profile in profiles %} 34 | 35 | 36 | 37 | 38 | 39 | {# #} 40 | 41 | {% endfor %} 42 | 43 |
Name# Associated ScansToolCommandDescription
{{ profile.name }}{{ profile.scans.count }}{{ profile.tool }}{{ profile.command }}{{ profile.description }}
44 | {% endif %} 45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/scan-create.html: -------------------------------------------------------------------------------- 1 | {% extends 'scanner/base.html' %} 2 | {% load staticfiles %} 3 | {% load bootstrap3 %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block title %}Add Scan{% endblock title %} 7 | 8 | 9 | {% block header %} 10 | {% endblock %} 11 | 12 | {% block breadcrumbs %} 13 | 18 | {% endblock breadcrumbs %} 19 | 20 | {% block content %} 21 | 24 | {% crispy form %} 25 | 26 | {% endblock %} 27 | 28 | {% block tail_js %} 29 | 30 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/scan-detail.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block breadcrumbs %} 5 | 10 | {% endblock breadcrumbs %} 11 | 12 | {% block content %} 13 | {# TODO: Emergency stop button/modal #} 14 |
15 |
16 |

Details

17 |
18 |
19 |
20 |
Name
21 |
{{ scan.name }}
22 |
23 |
24 |
Profile
25 |
26 | {{ scan.profile.name }}
27 | {{ scan.profile.tool }} {{ scan.profile.command }} 28 |
29 |
30 |
31 |
Targets
32 |
{{ scan.hosts|linebreaksbr }}
33 |
34 |
35 |
Parameters
36 |
Scan {{ scan.chunk_size }} IPs per scanner using {{ scan.scanner_count }} scanners
37 |
38 |
39 |
Dates
40 |
{{ scan.start_date }} to {{ scan.stop_date }}
41 |
42 |
43 |
Hours
44 |
{{ scan.scan_hours }}
45 |
46 |
47 |
Progress
48 |
{{ scan.progress.percentage }} ({{ scan.progress.completed }} complete, {{ scan.progress.pending }} pending, {{ scan.progress.failed }} failed, {{ scan.progress.executing }} executing)
49 |
50 |
51 |
52 | 53 |
54 |
55 |

Scanners

56 |
57 |
58 | {% if request.user.is_superuser %} 59 |
60 |
61 | 62 | 63 |
64 |
65 | {% endif %} 66 | {% if scan.scanners.count == 0 %} 67 | No scanners are present for this scan. 68 | {% else %} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {% for scanner in scan.scanners.all %} 80 | 81 | 82 | 83 | 84 | 85 | 86 | {% endfor %} 87 | 88 |
NameIPStatusActions
{{ scanner.name }}{{ scanner.ip }}{{ scanner.status }}
89 | {% endif %} 90 |
91 |
92 | 93 |
94 |
95 |

Jobs

96 |
97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | {% if request.user.is_superuser %}{% endif %} 105 | 106 | 107 | 108 | {% for job in scan.jobs.all %} 109 | 110 | 111 | 112 | 113 | {% if request.user.is_superuser %}{% endif %} 114 | 115 | {% endfor %} 116 | 117 |
Job ID# AttemptsStatusActions
{{ job.id }}{{ job.attempts }}{{ job.status }}
118 |
119 |
120 | 121 | {% comment %}
122 |
123 |

Files

124 |
125 |
126 | List of files. 127 |
128 |
{% endcomment %} 129 | 130 | {% endblock %} -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/scan-list.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %}Scans{% endblock title %} 5 | 6 | 7 | {% block breadcrumbs %} 8 | 12 | {% endblock breadcrumbs %} 13 | 14 | {% block content %} 15 |
16 |
17 |

Current Scans {% if request.user.is_superuser %}New Scan{% endif %}

18 |
19 |
20 | {% if scans.count == 0 %} 21 | No scans are listed in the database. 22 | {% else %} 23 | 24 | 25 | 26 | 27 | {% if request.user.is_superuser %} 28 | 29 | 30 | {% else %} 31 | 32 | {% endif %} 33 | 34 | 35 | 36 | {% if request.user.is_superuser %}{% endif %} 37 | 38 | 39 | 40 | {% for scan in scans %} 41 | 42 | 43 | {% if request.user.is_superuser %} 44 | 45 | 46 | {% else %} 47 | 48 | {% endif %} 49 | 50 | 51 | 52 | {% if request.user.is_superuser %} 53 | {% endif %} 57 | 58 | {% endfor %} 59 | 60 |
NameProgressActions
{{ scan.name }}{{ scan.progress.percentage }} 54 | {% if scan.status == scan.RUNNING or scan.status == scan.PENDING %}Hold{% endif %} 55 | {% if scan.status == scan.ON_HOLD %}Run{% endif %} 56 |
61 | {% endif %} 62 |
63 |
64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/scanner-list.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %}Scanners{% endblock title %} 5 | 6 | {% block breadcrumbs %} 7 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |

Scanners

17 |
18 |
19 | {% if scanners.count == 0 %} 20 | No scanners are listed in the database. 21 | {% else %} 22 | {% regroup scanners by scan.name as scan_list %} 23 | {% for scan in scan_list %} 24 |

{{ scan.grouper }}

25 | 26 | {% for scanner in scan.list %} 27 | 28 | 29 | 30 | 34 | 42 | 43 | {% endfor %} 44 |
{{ scanner.ip|default_if_none:"Unknown" }}{{ scanner.get_status_display }} 31 | 32 | {{ scanner.last_seen|date:"y/m/d" }}
{{ scanner.last_seen|time:"H:i:s" }}
33 |
35 | {% if scanner.executing_jobs.count == 0 %} 36 | No running jobs. 37 | {% endif %} 38 | {% for job in scanner.executing_jobs.all %} 39 | {{ job.id }} 40 | {% endfor %} 41 |
45 | {% endfor %} 46 | 47 | 48 | {% comment %} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% for scan in scan_list %} 59 | 60 | 61 | 62 | {% for scanner in scan.list %} 63 | 64 | 65 | 66 | 67 | 72 | 73 | {% endfor %} 74 | {% endfor %} 75 | 76 |
Test NameStatusCurrent Job
{{ scan.grouper }}
{{ scanner.ip|default_if_none:"Unknown" }}{{ scanner.scan.name }}{{ scanner.get_status_display }} 68 | {% for job in scanner.executing_jobs.all %} 69 | {{ job.id }} 70 | {% endfor %} 71 |
{% endcomment %} 77 | {% endif %} 78 |
79 |
80 | {% endblock %} -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/user-list.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %}Users{% endblock title %} 5 | 6 | {% block breadcrumbs %} 7 | 12 | {% endblock breadcrumbs %} 13 | 14 | {% block content %} 15 |
16 |
17 |

Users {% comment %}{% if request.user.is_superuser %}New User{% endif %}{% endcomment %}

18 |
19 |
20 | {% if users.count == 0 %} 21 | No users are listed in the database. 22 | {% else %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% for u in users %} 35 | 36 | 37 | 38 | 39 | 46 | 47 | 48 | {% endfor %} 49 | 50 |
NameUsernameEmailAdministratorLast Login
{{ u.get_full_name }}{{ u.username }}{{ u.email }} 40 | {% if u.is_superuser %} 41 | 42 | {% else %} 43 | 44 | {% endif %} 45 | {{ u.last_login }}
51 | {% endif %} 52 |
53 |
54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /portplow/scanner/templates/scanner/user-logs.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %}User Logs{% endblock title %} 5 | 6 | {% block breadcrumbs %} 7 | 12 | {% endblock breadcrumbs %} 13 | 14 | {% block content %} 15 |
16 |
17 |

User Logs

18 |
19 |
20 | {% if users.count == 0 %} 21 | Nobody has logged in yet. 22 | {% else %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% for log in logs %} 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% endfor %} 41 | 42 |
IPDate/TimeUsername
{{ log.ip }}{{ log.dt|date:"Y/m/d H:i:s" }}{{ log.username }}
43 | {% endif %} 44 |
45 |
46 | {% endblock %} -------------------------------------------------------------------------------- /portplow/scanner/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /portplow/scanner/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.shortcuts import HttpResponseRedirect, resolve_url 3 | from django.contrib.auth.views import login, logout, password_change 4 | 5 | from scanner import views 6 | 7 | urlpatterns = [ 8 | url(r'^$', lambda r: HttpResponseRedirect(resolve_url('portplow:scan-list'))), 9 | 10 | url(r'^scan$', views.scan_list, name="scan-list"), 11 | url(r'^scan/create$', views.scan_create, name="scan-create"), 12 | url(r'^scanners$', views.scanner_list, name="scanner-list"), 13 | 14 | url(r'^profile$', views.profile_list, name="profile-list"), 15 | url(r'^profile/create$', views.profile_create, name="profile-create"), 16 | url(r'^detail/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$', 17 | views.scan_details, name="scan-details"), 18 | 19 | url(r'^results/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$', 20 | views.scan_results, name="scan-results"), 21 | url(r'^export/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$', 22 | views.export_scan, name="scan-export"), 23 | url(r'^report/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$', 24 | views.scan_report, name="scan-report"), 25 | url(r'^results/parse/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/$', 26 | views.scan_process_results, name="scan-process"), 27 | 28 | url(r'^hold/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/$', 29 | views.scan_hold, name="scan-hold"), 30 | url(r'^resume/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/$', 31 | views.scan_resume, name="scan-resume"), 32 | 33 | # Scanner operations 34 | url(r'^scanner/add/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$', 35 | views.scanner_add, name="scanner-add"), 36 | url(r'^scanner/remove/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$', 37 | views.scanner_remove, name="scanner-remove"), 38 | 39 | # User Administration 40 | url(r'^users$', views.user_list, name="user-list"), 41 | url(r'^user-logs$', views.user_logs, name="user-logs"), 42 | url(r'^groups$', views.group_list, name="group-list"), 43 | 44 | # Page for scanners to retrieve deconfliction notices. 45 | url(r'^deconfliction/(?P[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$', 46 | views.deconfliction_message, name='deconfliction_message'), 47 | 48 | # Authentication 49 | url(r'^login$', login, name='login', 50 | kwargs={'template_name': 'scanner/login.html'}), 51 | url(r'^logout$', logout, name='logout', 52 | kwargs={'next_page': '/'}), 53 | ] 54 | -------------------------------------------------------------------------------- /portplow/scanner/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required, permission_required, user_passes_test 2 | from django.http import HttpResponse, HttpResponseRedirect 3 | from django.shortcuts import render, get_object_or_404, redirect, resolve_url 4 | from django.views.decorators.cache import cache_page 5 | from django.contrib import messages 6 | from django.db.models import Count 7 | 8 | from scanner.models import Scan, Scanner, Profile, User, LogUser, Group, ScanResult, JobLog, Job 9 | from scanner.forms import ScanForm, ProfileForm 10 | from scanner.signals import hold_scan, resume_scan, add_scanner, remove_scanner 11 | from datetime import datetime 12 | from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 13 | from io import BytesIO, StringIO 14 | from django.core.paginator import Paginator 15 | import csv 16 | 17 | 18 | @login_required 19 | @user_passes_test(lambda u: u.is_superuser) 20 | def scan_create(request, template_name="scanner/scan-create.html"): 21 | scan_form = ScanForm(request.POST or None) 22 | if request.method == "POST": 23 | if scan_form.is_valid(): 24 | print("Got a valid form.") 25 | scan = scan_form.save(commit=False) 26 | scan.user = request.user 27 | scan.status = Scan.SETTING_UP 28 | scan.save() 29 | messages.success(request, "Scan created successfully. Setup will occur in the background " 30 | "and will be ready shortly.") 31 | return redirect('portplow:scan-list') 32 | else: 33 | print("Invalid form received.") 34 | for field in scan_form: 35 | print("{} -- {}".format(field, field.errors)) 36 | 37 | return render(request, template_name, {'form': scan_form}) 38 | 39 | 40 | @login_required 41 | @user_passes_test(lambda u: u.is_superuser) 42 | def scan_hold(request, scan_id=None): 43 | scan = get_object_or_404(Scan, id=scan_id) 44 | if scan.status != scan.RUNNING and scan.status != scan.PENDING: 45 | messages.error(request, "Scan is not currently running.") 46 | else: 47 | messages.success(request, "Scan is now on hold.") 48 | hold_scan(scan) 49 | 50 | return HttpResponseRedirect(resolve_url("portplow:scan-list")) 51 | 52 | 53 | @login_required 54 | @user_passes_test(lambda u: u.is_superuser) 55 | def scan_resume(request, scan_id=None): 56 | scan = get_object_or_404(Scan, id=scan_id) 57 | if scan.status != scan.ON_HOLD: 58 | messages.error(request, "Scan is not currently on hold.") 59 | else: 60 | messages.success(request, "Scan is now on running.") 61 | resume_scan(scan) 62 | 63 | return HttpResponseRedirect(resolve_url("portplow:scan-list")) 64 | 65 | 66 | @login_required 67 | @user_passes_test(lambda u: u.is_superuser) 68 | def profile_create(request, template_name="scanner/profile-create.html"): 69 | profile_form = ProfileForm(request.POST or None) 70 | if request.method == "POST": 71 | if profile_form.is_valid(): 72 | print("Got a valid form.") 73 | profile_form.save() 74 | return redirect('portplow:profile-list') 75 | else: 76 | print("Invalid form received.") 77 | for field in profile_form: 78 | print("{} -- {}".format(field, field.errors)) 79 | 80 | return render(request, template_name, {'form': profile_form}) 81 | 82 | 83 | @login_required 84 | def profile_list(request): 85 | profiles = Profile.objects.all() 86 | 87 | ctx = { 88 | "profiles": profiles 89 | } 90 | return render(request, template_name="scanner/profile-list.html", context=ctx) 91 | 92 | 93 | @login_required 94 | def scanner_list(request): 95 | if request.user.is_staff: 96 | scanners = Scanner.objects.all() 97 | else: 98 | scanners = Scanner.objects.filter(scan__group__in=request.user.groups.all()).all() 99 | ctx = { 100 | "scanners": scanners.order_by('scan__name', 'status', 'ip') 101 | } 102 | return render(request, template_name="scanner/scanner-list.html", context=ctx) 103 | 104 | 105 | @login_required 106 | @user_passes_test(lambda u: u.is_superuser) 107 | def user_list(request): 108 | users = User.objects.all().order_by('-is_superuser', 'last_name', 'first_name') 109 | 110 | ctx = { 111 | "users": users 112 | } 113 | return render(request, template_name="scanner/user-list.html", context=ctx) 114 | 115 | 116 | @login_required 117 | def group_list(request): 118 | groups = Group.objects.all() 119 | 120 | ctx = { 121 | "groups": groups 122 | } 123 | return render(request, template_name="scanner/group-list.html", context=ctx) 124 | 125 | 126 | 127 | @login_required 128 | def scan_list(request): 129 | if request.user.is_staff: 130 | scans = Scan.objects.all() 131 | else: 132 | scans = Scan.objects.filter(group__in=request.user.groups.all()).all() 133 | 134 | ctx = { 135 | "scans": scans.order_by('name') 136 | } 137 | return render(request, template_name="scanner/scan-list.html", context=ctx) 138 | 139 | 140 | @login_required 141 | # @cache_page(60) 142 | def scan_details(request, scan_id=None): 143 | 144 | if request.user.is_staff: 145 | scan = get_object_or_404(Scan, id=scan_id) 146 | else: 147 | queryset = Scan.objects.filter(group__in=request.user.groups.all()) 148 | scan = get_object_or_404(queryset, id=scan_id) 149 | 150 | port_counts = ScanResult.objects.filter(scan=scan, state="open").values('port').annotate(total=Count('id')) 151 | 152 | ctx = { 153 | "scan": scan, 154 | "port_counts": port_counts 155 | } 156 | 157 | return render(request, template_name="scanner/scan-detail-all-jobs.html", context=ctx) 158 | 159 | 160 | @login_required 161 | @user_passes_test(lambda u: u.is_superuser) 162 | def scan_process_results(request, scan_id=None): 163 | print("Called scan_process_results") 164 | from scanner.models import JobLog 165 | from scanner.tasks import parse_results 166 | 167 | if request.user.is_staff: 168 | scan = get_object_or_404(Scan, id=scan_id) 169 | else: 170 | queryset = Scan.objects.filter(group__in=request.user.groups) 171 | scan = get_object_or_404(queryset, id=scan_id) 172 | 173 | print("Found scan.") 174 | unparsed = JobLog.objects.filter(job__scan=scan, parsed=False).all() 175 | if unparsed.count() > 0: 176 | parse_results.delay(scan_id=scan_id) 177 | messages.success(request, "Queued job to parse scan results.") 178 | else: 179 | messages.error(request, "No results to process at this time.") 180 | 181 | print("Checked for unparsed.") 182 | return HttpResponseRedirect(resolve_url("portplow:scan-details", scan_id=scan_id)) 183 | 184 | 185 | @login_required 186 | @permission_required("scan.view_results") 187 | def scan_results(request, scan_id=None): 188 | 189 | if request.user.is_staff: 190 | scan = get_object_or_404(Scan, id=scan_id) 191 | else: 192 | queryset = Scan.objects.filter(group__in=request.user.groups) 193 | scan = get_object_or_404(queryset, id=scan_id) 194 | 195 | ctx = { 196 | "scan": scan, 197 | } 198 | return render(request, template_name="scanner/scan-results.html", context=ctx) 199 | 200 | 201 | @login_required 202 | @user_passes_test(lambda u: u.is_superuser) 203 | def user_logs(request, template_name="scanner/user-logs.html"): 204 | logs = LogUser.objects.all() 205 | return render(request, template_name, context={"logs": logs}) 206 | 207 | 208 | @login_required 209 | @user_passes_test(lambda u: u.is_superuser) 210 | def scanner_add(request, scan_id=None): 211 | scan = get_object_or_404(Scan, id=scan_id) 212 | if add_scanner(scan): 213 | messages.success(request, "Scanner added successfully.") 214 | else: 215 | messages.error(request, "Unable to add scanner.") 216 | 217 | return HttpResponseRedirect(resolve_url('portplow:scan-details', scan_id=scan_id)) 218 | 219 | 220 | @login_required 221 | @user_passes_test(lambda u: u.is_superuser) 222 | def scanner_remove(request, scanner_id=None): 223 | scanner = get_object_or_404(Scanner, id=scanner_id) 224 | if remove_scanner(scanner): 225 | messages.success(request, "Scanner removed successfully.") 226 | else: 227 | messages.error(request, "Unable to remove scanner.") 228 | 229 | return HttpResponseRedirect(resolve_url('portplow:scan-details', scan_id=scanner.scan_id)) 230 | 231 | 232 | @login_required 233 | @user_passes_test(lambda u: u.is_superuser) 234 | def export_scan(request, scan_id=None): 235 | """ 236 | Export all XML files associated with a scan. 237 | """ 238 | scan = get_object_or_404(Scan, id=scan_id) 239 | 240 | in_memory = BytesIO() 241 | zip = ZipFile(in_memory, 'w', ZIP_DEFLATED, allowZip64=True) 242 | paginator = Paginator(JobLog.objects.filter(return_code=0, 243 | job__scan=scan).only('start_time', 'id', 244 | 'job__target_hosts', 245 | 'stdout').order_by('id').all(), 2500) 246 | for page in paginator.page_range: 247 | for result in paginator.page(page).object_list: 248 | if result.start_time is not None: 249 | dt = result.start_time.timetuple() 250 | folder_dt = result.start_time.strftime("%Y-%m-%d") 251 | else: 252 | dt = datetime.utcnow().timetuple() 253 | folder_dt = "unparsed" 254 | info = ZipInfo("scans/{}/{}".format(folder_dt, str(result.id)), 255 | date_time=dt) 256 | info.compress_type= ZIP_DEFLATED 257 | info.comment = bytes(result.job.target_hosts, 'utf-8') 258 | info.create_system = 0 259 | zip.writestr(info, result.stdout) # 'utf-8')) 260 | 261 | zip.close() 262 | zip_name = "{} - Scan Export {}".\ 263 | format(datetime.now().strftime("%Y-%m-%d_%H%M%S"), scan.name) 264 | 265 | response = HttpResponse(content_type="application/zip") 266 | response["Content-Disposition"] = "attachment; filename={}.zip".format(zip_name) 267 | response.write(in_memory.getvalue()) 268 | return response 269 | 270 | 271 | @login_required 272 | @user_passes_test(lambda u: u.is_superuser) 273 | def scan_report(request, scan_id=None): 274 | scan = get_object_or_404(Scan, id=scan_id) 275 | 276 | results = ScanResult.objects.filter(scan=scan).values('ip', 'state', 'reason', 'port').all() 277 | 278 | in_memory = StringIO() 279 | 280 | csv_name = "{} - Scan Export {}". \ 281 | format(datetime.now().strftime("%Y-%m-%d_%H%M%S"), scan.name) 282 | 283 | fields = ['ip', 'port', 'state', 'reason'] 284 | output = csv.DictWriter(in_memory, fieldnames=fields) 285 | output.writeheader() 286 | output.writerows(results) 287 | 288 | response = HttpResponse(content_type="text/csv") 289 | response["Content-Disposition"] = "attachment; filename={}.csv".format(csv_name) 290 | response.write(in_memory.getvalue()) 291 | return response 292 | 293 | 294 | def deconfliction_message(request, scanner_id=None): 295 | """ 296 | Provides the deconfliction message used for any given scan. This is 297 | used to update the individual scanners' deconfliction pages. 298 | """ 299 | scanner = get_object_or_404(Scanner, id=scanner_id) 300 | message = scanner.scan.deconfliction_message 301 | 302 | if message is not None: 303 | return HttpResponse(message) 304 | else: 305 | return HttpResponse("404 not found.", status=404) 306 | -------------------------------------------------------------------------------- /portplow/templates/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /opt/portplow 4 | mkdir -p /var/opt/portplow 5 | 6 | # Set timezone to UTC 7 | rm /etc/localtime 8 | ln -s /usr/share/zoneinfo/UTC /etc/localtime 9 | 10 | DEBIAN_FRONTEND=noninteractive 11 | 12 | # Make alias to metadata storage endpoint 13 | # http://169.254.169.254/metadata/v1/ 14 | echo "169.254.169.254 infoinfo" >> /etc/hosts 15 | 16 | # Allow IPTables-Persistent to install automatically 17 | echo "iptables-persistent iptables-persistent/autosave_v4 boolean true" | /usr/bin/debconf-set-selections 18 | echo "iptables-persistent iptables-persistent/autosave_v6 boolean true" | /usr/bin/debconf-set-selections 19 | 20 | apt-get update 21 | apt-get dist-upgrade -y 22 | 23 | apt-get install -y git gcc make libpcap-dev nmap python3-pip tmux python3-dev supervisor python3 nginx iptables-persistent 24 | 25 | # Install masscan 26 | git clone https://github.com/robertdavidgraham/masscan.git /opt/portplow/masscan 27 | cd /opt/portplow/masscan/ 28 | make -j 29 | 30 | # Setup environment for client 31 | pip3 install requests coloredlogs 32 | 33 | echo -n {{CLIENT_SCRIPT}} | base64 -d > /opt/portplow/client.py 34 | 35 | cat >/etc/supervisor/conf.d/portplow-client.conf <<"EOF" 36 | [program:portplow] 37 | command=/usr/bin/python /opt/portplow/client.py 38 | startretries=10 39 | autorestart=true 40 | environment=PORTPLOW_URL="{{SERVICE_URL}}",PORTPLOW_TOKEN="{{API_TOKEN}}",PORTPLOW_DELAY="{{DELAY}}",PORTPLOW_DIR="{{DIR}}" 41 | EOF 42 | 43 | cat >/opt/portplow/envsettings <<"EOF" 44 | export PORTPLOW_URL="{{SERVICE_URL}}" 45 | export PORTPLOW_TOKEN="{{API_TOKEN}}" 46 | export PORTPLOW_DELAY="{{DELAY}}" 47 | export PORTPLOW_DIR="{{DIR}}" 48 | EOF 49 | 50 | cat >/etc/nginx/sites-enabled/default <<"EOF" 51 | server { 52 | listen 80 default_server; 53 | listen [::]:80 default_server ipv6only=on; 54 | 55 | root /var/www/html; 56 | index index.html index.htm; 57 | 58 | server_name localhost; 59 | 60 | location / { 61 | try_files $uri $uri/ =404; 62 | # auth_basic "Restricted Content"; 63 | # auth_basic_user_file /etc/nginx/.htpasswd; 64 | } 65 | } 66 | EOF 67 | 68 | mkdir -p /var/www/html 69 | cat >/var/www/html/index.html <<"EOF" 70 | {{SEC_MESSAGE}} 71 | EOF 72 | 73 | chown -R www:www -R /var/www/ 74 | 75 | # IP Tables setup 76 | NETWORKS="{{NETWORKS}} 127.0.0.0/8 196.227.250.250/32" 77 | /sbin/iptables -F 78 | 79 | for net in $NETWORKS 80 | do 81 | /sbin/iptables -A INPUT -p tcp --dport 80 -s $net -j ACCEPT 82 | done 83 | /sbin/iptables -A INPUT -p tcp --dport 80 -j DROP 84 | 85 | /etc/init.d/iptables-persistent save 86 | 87 | # Create cron job to update security message 88 | cat >/usr/bin/update_sec_message <<"EOF" 89 | curl -L {{DECONFLICTION_URL}} > /var/www/html/index.html 90 | chmod +r /var/www/html/index.html 91 | EOF 92 | 93 | chmod +x /usr/bin/update_sec_message 94 | 95 | /usr/bin/update_sec_message 96 | 97 | # Add job update to crontab 98 | line="0,30 * * * * /usr/bin/update_sec_message" 99 | (crontab -u root -l; echo "$line" ) | crontab -u root - 100 | 101 | reboot -------------------------------------------------------------------------------- /portplow/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/utils/__init__.py -------------------------------------------------------------------------------- /portplow/utils/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UtilsConfig(AppConfig): 5 | name = 'utils' 6 | -------------------------------------------------------------------------------- /portplow/utils/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from crispy_forms.helper import FormHelper 3 | from crispy_forms.layout import Layout, Submit, Button, Field, Hidden 4 | from crispy_forms.bootstrap import FormActions, AppendedText, TabHolder, Tab 5 | from django.contrib.auth import get_user_model 6 | 7 | User = get_user_model() 8 | 9 | 10 | class PasswordResetRequestForm(forms.Form): 11 | email_or_username = forms.CharField(label="Email", max_length=254) 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(PasswordResetRequestForm, self).__init__(*args, **kwargs) 15 | self.helper = FormHelper() 16 | self.helper.form_class = 'form-horizontal' 17 | self.helper.label_class = 'col-lg-2' 18 | self.helper.field_class = 'col-lg-8' 19 | self.helper.layout = Layout( 20 | 'email_or_username', 21 | FormActions( 22 | Submit('submit', 'Reset Password') 23 | ) 24 | ) 25 | 26 | class SetPasswordForm(forms.Form): 27 | """ 28 | A form that lets a user change set their password without entering the old 29 | password 30 | """ 31 | error_messages = { 32 | 'password_mismatch': "The two password fields didn't match." 33 | } 34 | new_password1 = forms.CharField(label="New password", 35 | widget=forms.PasswordInput) 36 | new_password2 = forms.CharField(label="New password confirmation", 37 | widget=forms.PasswordInput) 38 | 39 | def __init__(self, *args, **kwargs): 40 | super(SetPasswordForm, self).__init__(*args, **kwargs) 41 | self.helper = FormHelper() 42 | self.helper.form_class = 'form-horizontal' 43 | self.helper.label_class = 'col-lg-2' 44 | self.helper.field_class = 'col-lg-8' 45 | self.helper.layout = Layout( 46 | 'new_password1', 47 | 'new_password2', 48 | FormActions( 49 | Submit('submit', 'Reset Password') 50 | ) 51 | ) 52 | 53 | def clean_new_password2(self): 54 | password1 = self.cleaned_data.get('new_password1') 55 | password2 = self.cleaned_data.get('new_password2') 56 | if password1 and password2: 57 | if password1 != password2: 58 | raise forms.ValidationError( 59 | self.error_messages['password_mismatch'], 60 | code='password_mismatch', 61 | ) 62 | return password2 63 | 64 | 65 | class UserForm(forms.ModelForm): 66 | 67 | class Meta: 68 | model = User 69 | fields = ['first_name', 'last_name', 'username', 'email'] 70 | 71 | def __init__(self, *args, **kwargs): 72 | super(UserForm, self).__init__(*args, **kwargs) 73 | self.helper = FormHelper() 74 | self.helper.form_class = 'form-horizontal' 75 | self.helper.label_class = 'col-lg-2' 76 | self.helper.field_class = 'col-lg-8' 77 | self.helper.layout = Layout( 78 | 'first_name', 'last_name', 79 | 'username', 80 | 'email', 81 | FormActions( 82 | Submit('submit', 'Add User') 83 | ) 84 | ) -------------------------------------------------------------------------------- /portplow/utils/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threatexpress/portplow/370ec8d26022114f81f1a587e6bae0dcd9e63352/portplow/utils/migrations/__init__.py -------------------------------------------------------------------------------- /portplow/utils/templates/utils/password_reset_email-utils.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} 3 | 4 | {% trans "Please go to the following page and choose a new password:" %} 5 | {% block reset_link %} 6 | {{ domain }}{% url 'utils:reset_password_confirm' uidb64=uid token=token %} 7 | {% endblock %} 8 | 9 | {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} 10 | 11 | {% trans "Thanks for using our site!" %} 12 | 13 | {% blocktrans %}The MINIS, LLC team{% endblocktrans %} 14 | 15 | {% endautoescape %} -------------------------------------------------------------------------------- /portplow/utils/templates/utils/password_reset_form-utils.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base_no-header.html" %} 2 | {% load staticfiles %} 3 | {% load bootstrap3 %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block content %} 7 | 8 | 9 |
10 |
11 | {% crispy form %} 12 |
13 |
14 | {% endblock content %} -------------------------------------------------------------------------------- /portplow/utils/templates/utils/user_add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "scanner/base.html" %} 2 | {% load staticfiles %} 3 | {% load bootstrap3 %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block title %}Add User{% endblock %} 7 | {% block content %} 8 | 9 | 10 |
11 |
12 | {% crispy form %} 13 |
14 |
15 | {% endblock content %} -------------------------------------------------------------------------------- /portplow/utils/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /portplow/utils/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from utils.views import ResetPasswordRequestView, PasswordResetConfirmView, logout, UserAddView 3 | 4 | urlpatterns = [ 5 | url(r'^reset_password_confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', PasswordResetConfirmView.as_view(), 6 | name='reset_password_confirm'), 7 | url(r'^reset_password', ResetPasswordRequestView.as_view(), name="reset_password"), 8 | url(r'^user/add', UserAddView.as_view(), name="add_user"), 9 | # url(r'^logout', logout, name="logout") 10 | ] 11 | -------------------------------------------------------------------------------- /portplow/utils/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.contrib.auth.tokens import default_token_generator 3 | from django.utils.encoding import force_bytes 4 | from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode 5 | from django.template import loader 6 | from django.core.validators import validate_email 7 | from django.core.exceptions import ValidationError 8 | from django.core.mail import send_mail 9 | from portplow.settings import DEFAULT_FROM_EMAIL 10 | from django.views.generic import * 11 | from utils.forms import PasswordResetRequestForm, SetPasswordForm, UserForm 12 | from django.contrib import messages 13 | from django.contrib.auth import get_user_model, logout 14 | from django.shortcuts import HttpResponseRedirect, resolve_url 15 | from django.db.models.query_utils import Q 16 | from portplow.settings import LOGIN_URL, DOMAIN, SITE_NAME 17 | from scanner.models import Group, generate_random_password 18 | from django.utils.decorators import method_decorator 19 | from django.contrib.auth.decorators import login_required, user_passes_test 20 | 21 | User = get_user_model() 22 | 23 | 24 | class ResetPasswordRequestView(FormView): 25 | template_name = "utils/password_reset_form-utils.html" 26 | success_url = LOGIN_URL 27 | form_class = PasswordResetRequestForm 28 | log = logging.getLogger(__name__) 29 | 30 | @staticmethod 31 | def validate_email_address(email): 32 | try: 33 | validate_email(email) 34 | return True 35 | except ValidationError: 36 | return False 37 | 38 | def post(self, request, *args, **kwargs): 39 | form = self.form_class(request.POST) 40 | self.log.debug("Received a post.") 41 | try: 42 | if form.is_valid(): 43 | data = form.cleaned_data["email_or_username"] 44 | 45 | if self.validate_email_address(data) is True: 46 | ''' 47 | If the input is an valid email address, then the following code will lookup for users associated with 48 | that email address. If found then an email will be sent to the address, else an error message will be 49 | printed on the screen. 50 | ''' 51 | self.log.debug("Email is valid.") 52 | associated_users = User.objects.filter(Q(email=data)|Q(username=data)) 53 | if associated_users.exists(): 54 | self.log.debug("The user exists.") 55 | for user in associated_users: 56 | c = { 57 | 'email': user.email, 58 | 'domain': request.META['HTTP_HOST'], 59 | 'site_name': 'your site', 60 | 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 61 | 'user': user, 62 | 'token': default_token_generator.make_token(user), 63 | 'protocol': 'https', 64 | } 65 | subject_template_name='registration/password_reset_subject.txt' 66 | email_template_name='utils/password_reset_email-utils.html' 67 | subject = loader.render_to_string(subject_template_name, c) 68 | subject = ''.join(subject.splitlines()) 69 | email = loader.render_to_string(email_template_name, c) 70 | self.log.debug("Ready to send email. {}".format(email)) 71 | send_mail(subject, email, DEFAULT_FROM_EMAIL, [user.email], fail_silently=False) 72 | self.log.debug("Email has been sent.") 73 | result = self.form_valid(form) 74 | messages.success(request, 'An email has been sent to ' + data + ". Please check its inbox to continue reseting password.") 75 | self.log.debug("Finished request. Result: {}".format(result)) 76 | return result 77 | else: 78 | result = self.form_invalid(form) 79 | messages.error(request, 'Username or Email not found.') 80 | return result 81 | else: 82 | ''' 83 | If the input is an username, then the following code will lookup for users associated with that user. 84 | If found then an email will be sent to the user's address, else an error message will be printed on 85 | the screen. 86 | ''' 87 | associated_users = User.objects.filter(username=data) 88 | if associated_users.exists(): 89 | for user in associated_users: 90 | c = { 91 | 'email': user.email, 92 | 'domain': DOMAIN, 93 | 'site_name': SITE_NAME, 94 | 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 95 | 'user': user, 96 | 'token': default_token_generator.make_token(user), 97 | 'protocol': 'https', 98 | } 99 | subject_template_name='registration/password_reset_subject.txt' 100 | email_template_name='utils/password_reset_email-utils.html' 101 | subject = loader.render_to_string(subject_template_name, c) 102 | # Email subject *must not* contain newlines 103 | subject = ''.join(subject.splitlines()) 104 | email = loader.render_to_string(email_template_name, c) 105 | send_mail(subject, email, DEFAULT_FROM_EMAIL , [user.email], fail_silently=False) 106 | result = self.form_valid(form) 107 | messages.success(request, 'Email has been sent to ' + data +"'s email address. Please check its inbox to continue reseting password.") 108 | return result 109 | result = self.form_invalid(form) 110 | messages.error(request, 'Username or email not found.') 111 | return result 112 | 113 | messages.error(request, 'Invalid Input') 114 | 115 | except Exception as e: 116 | self.log.error(e) 117 | 118 | return self.form_invalid(form) 119 | 120 | 121 | class PasswordResetConfirmView(FormView): 122 | template_name = "utils/password_reset_form-utils.html" 123 | success_url = '/' 124 | form_class = SetPasswordForm 125 | 126 | def post(self, request, uidb64=None, token=None, *arg, **kwargs): 127 | """ 128 | View that checks the hash in a password reset link and presents a 129 | form for entering a new password. 130 | """ 131 | UserModel = get_user_model() 132 | form = self.form_class(request.POST) 133 | assert uidb64 is not None and token is not None # checked by URLconf 134 | try: 135 | uid = urlsafe_base64_decode(uidb64) 136 | user = UserModel._default_manager.get(pk=uid) 137 | except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist): 138 | user = None 139 | 140 | if user is not None and default_token_generator.check_token(user, token): 141 | if form.is_valid(): 142 | new_password = form.cleaned_data['new_password2'] 143 | user.set_password(new_password) 144 | user.save() 145 | messages.success(request, 'Password has been reset.') 146 | return self.form_valid(form) 147 | else: 148 | messages.error(request, 'Password reset has not been unsuccessful.') 149 | return self.form_invalid(form) 150 | else: 151 | messages.error(request,'The reset password link is no longer valid.') 152 | return self.form_invalid(form) 153 | 154 | 155 | 156 | # @method_decorator(user_passes_test, lambda u: u.is_superuser) 157 | @method_decorator(login_required, name="dispatch") 158 | class UserAddView(FormView): 159 | template_name = "utils/user_add_form.html" 160 | form_class = UserForm 161 | success_url = '/portplow/users' 162 | 163 | def post(self, request, *args, **kwargs): 164 | UserModel = get_user_model() 165 | form = self.form_class(request.POST or None) 166 | if form.is_valid(): 167 | user = form.save(commit=False) 168 | user.password = generate_random_password() 169 | user.save() 170 | user.refresh_from_db() 171 | user.groups.add(Group.objects.first()) 172 | user.save() 173 | return self.form_valid(form) 174 | else: 175 | messages.error(request, "Invalid information provided.") 176 | return self.form_invalid(form) 177 | 178 | 179 | def logout_view(request): 180 | logout(request) 181 | return HttpResponseRedirect("/") 182 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # PortPlow.io 2 | 3 | Manage large-scale distributed scans with a web front end. This project was built to be run on a DigitalOcean 2-4GB RAM system. 4 | 5 | ## Setup instructions 6 | - Setup DNS A record pointing to your DigitalOcean Droplet. 7 | - Download the portplow repo to the droplet 8 | - Extract the repo to the `/opt` directory 9 | - Modify the settings in the conf file 10 | - Execute `./installer.sh` 11 | 12 | This will setup and configure nginx, postgres, redis, letsencrypt and Django. 13 | --------------------------------------------------------------------------------