From 90ab6a37fbfa60cf3f89abaf5a142bffa5d21a32 Mon Sep 17 00:00:00 2001 From: Graham Gilbert Date: Mon, 17 Aug 2015 20:33:21 +0100 Subject: [PATCH] First look at inventory --- inventory/__init__.py | 0 inventory/admin.py | 11 ++ inventory/migrations/0001_initial.py | 41 ++++ inventory/migrations/__init__.py | 0 inventory/models.py | 21 ++ inventory/templates/inventory/index.html | 70 +++++++ inventory/tests.py | 3 + inventory/urls.py | 10 + inventory/views.py | 183 ++++++++++++++++++ sal/system_settings.py | 1 + sal/urls.py | 3 +- server/migrations/0014_auto_20150817_1646.py | 20 ++ .../plugins/topprocesses/templates/front.html | 4 +- server/plugins/topprocesses/templates/id.html | 4 +- server/plugins/topprocesses/topprocesses.py | 3 +- server/templates/server/bu_dashboard.html | 1 + server/templates/server/group_dashboard.html | 2 +- server/templates/server/index.html | 3 + 18 files changed, 374 insertions(+), 6 deletions(-) create mode 100644 inventory/__init__.py create mode 100644 inventory/admin.py create mode 100644 inventory/migrations/0001_initial.py create mode 100644 inventory/migrations/__init__.py create mode 100644 inventory/models.py create mode 100644 inventory/templates/inventory/index.html create mode 100644 inventory/tests.py create mode 100644 inventory/urls.py create mode 100644 inventory/views.py create mode 100644 server/migrations/0014_auto_20150817_1646.py diff --git a/inventory/__init__.py b/inventory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/inventory/admin.py b/inventory/admin.py new file mode 100644 index 00000000..d594b496 --- /dev/null +++ b/inventory/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from inventory.models import Inventory, InventoryItem + +class InventoryAdmin(admin.ModelAdmin): + list_display = ('machine', 'datestamp', 'sha256hash') + +class InventoryItemAdmin(admin.ModelAdmin): + list_display = ('name', 'version', 'path') + +admin.site.register(Inventory, InventoryAdmin) +admin.site.register(InventoryItem, InventoryItemAdmin) \ No newline at end of file diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py new file mode 100644 index 00000000..2d260153 --- /dev/null +++ b/inventory/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('server', '0014_auto_20150817_1646'), + ] + + operations = [ + migrations.CreateModel( + name='Inventory', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('datestamp', models.DateTimeField(auto_now=True)), + ('sha256hash', models.CharField(max_length=64)), + ('machine', models.ForeignKey(to='server.Machine')), + ], + options={ + 'ordering': ['datestamp'], + }, + ), + migrations.CreateModel( + name='InventoryItem', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=255)), + ('version', models.CharField(max_length=32)), + ('bundleid', models.CharField(max_length=255)), + ('bundlename', models.CharField(max_length=255)), + ('path', models.TextField()), + ('machine', models.ForeignKey(to='server.Machine')), + ], + options={ + 'ordering': ['name', '-version'], + }, + ), + ] diff --git a/inventory/migrations/__init__.py b/inventory/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/inventory/models.py b/inventory/models.py new file mode 100644 index 00000000..f8d4eadd --- /dev/null +++ b/inventory/models.py @@ -0,0 +1,21 @@ +from django.db import models +from server.models import * +# Create your models here. + +class Inventory(models.Model): + machine = models.ForeignKey(Machine) + datestamp = models.DateTimeField(auto_now=True) + sha256hash = models.CharField(max_length=64) + class Meta: + ordering = ['datestamp'] + + +class InventoryItem(models.Model): + machine = models.ForeignKey(Machine) + name = models.CharField(max_length=255) + version = models.CharField(max_length=32) + bundleid = models.CharField(max_length=255) + bundlename = models.CharField(max_length=255) + path = models.TextField() + class Meta: + ordering = ['name', '-version'] \ No newline at end of file diff --git a/inventory/templates/inventory/index.html b/inventory/templates/inventory/index.html new file mode 100644 index 00000000..bd307cc6 --- /dev/null +++ b/inventory/templates/inventory/index.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} +{% load i18n %} +{% load dashboard_extras %} + +{% block script %} + + +{% endblock %} + +{% block nav %} +
  • Back
  • + + +{% endblock %} +{% block content %} + +
    + +
    + Application Inventory +
    + +
    +
    + + + + + + + + + + {% for item in inventory|dictsort:'name' %} + + + + + + + {% endfor %} + +
    NameVersionBundle ID
    + + {{ item.name }} + + {{ item.version }}{{ item.bundleid }}
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/inventory/tests.py b/inventory/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/inventory/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/inventory/urls.py b/inventory/urls.py new file mode 100644 index 00000000..1544d30e --- /dev/null +++ b/inventory/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from inventory import views + +urlpatterns = [ + url(r'^submit/$', views.inventory_submit), + url(r'^hash/(?P.+)/$', views.inventory_hash), + url(r'^business_unit/(?P.+)/$', views.bu_inventory), + url(r'^machine_group/(?P.+)/$', views.machine_group_inventory), + url(r'^$', views.index), +] diff --git a/inventory/views.py b/inventory/views.py new file mode 100644 index 00000000..88bc2bbf --- /dev/null +++ b/inventory/views.py @@ -0,0 +1,183 @@ +from django.http import HttpResponse, HttpRequest, HttpResponseRedirect +from django.template import RequestContext, Template, Context +from django.shortcuts import render_to_response +from django.core.context_processors import csrf +from django.views.decorators.csrf import csrf_exempt +from django.core.urlresolvers import reverse +from django.http import Http404 +#from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required, permission_required +from django.conf import settings +from django import forms +from django.db.models import Q +from django.db.models import Count +from server import utils +from django.shortcuts import render_to_response, get_object_or_404, redirect + +import plistlib +import base64 +import bz2 +import hashlib +import json + +from datetime import datetime +import urllib2 +from xml.etree import ElementTree + +from models import * +from server.models import * + +def decode_to_string(base64bz2data): + '''Decodes an inventory submission, which is a plist-encoded + list, compressed via bz2 and base64 encoded.''' + try: + bz2data = base64.b64decode(base64bz2data) + return bz2.decompress(bz2data) + except Exception: + return '' + +def unique_apps(inventory): + found = [] + for inventory_item in inventory: + found_flag = False + for found_item in found: + if (inventory_item.name == found_item['name'] and + inventory_item.version == found_item['version'] and + inventory_item.bundleid == found_item['bundleid'] and + inventory_item.bundlename == found_item['bundlename'] and + inventory_item.path == found_item['path']): + found_flag = True + break + if found_flag == False: + found_item = {} + found_item['name'] = inventory_item.name + found_item['version'] = inventory_item.version + found_item['bundleid'] = inventory_item.bundleid + found_item['bundlename'] = inventory_item.bundlename + found_item['path'] = inventory_item.path + found.append(found_item) + return found + +@csrf_exempt +def inventory_submit(request): + if request.method != 'POST': + raise Http404 + + # list of bundleids to ignore + bundleid_ignorelist = [ + 'com.apple.print.PrinterProxy' + ] + submission = request.POST + serial = submission.get('serial') + machine = None + if serial: + try: + machine = Machine.objects.get(serial=serial) + except Machine.DoesNotExist: + raise Http404 + + compressed_inventory = submission.get('base64bz2inventory') + if compressed_inventory: + compressed_inventory = compressed_inventory.replace(" ", "+") + inventory_str = decode_to_string(compressed_inventory) + try: + inventory_list = plistlib.readPlistFromString(inventory_str) + except Exception: + inventory_list = None + if inventory_list: + try: + inventory_meta = Inventory.objects.get(machine=machine) + except Inventory.DoesNotExist: + inventory_meta = Inventory(machine=machine) + inventory_meta.sha256hash = \ + hashlib.sha256(inventory_str).hexdigest() + # clear existing inventoryitems + machine.inventoryitem_set.all().delete() + # insert current inventory items + for item in inventory_list: + # skip items in bundleid_ignorelist. + if not item.get('bundleid') in bundleid_ignorelist: + i_item = machine.inventoryitem_set.create( + name=item.get('name', ''), + version=item.get('version', ''), + bundleid=item.get('bundleid', ''), + bundlename=item.get('CFBundleName', ''), + path=item.get('path', '') + ) + machine.last_inventory_update = datetime.now() + inventory_meta.save() + machine.save() + return HttpResponse( + "Inventory submmitted for %s.\n" % + submission.get('serial')) + + return HttpResponse("No inventory submitted.\n") + + +def inventory_hash(request, serial): + sha256hash = '' + machine = None + if serial: + try: + machine = Machine.objects.get(serial=serial) + inventory_meta = Inventory.objects.get(machine=machine) + sha256hash = inventory_meta.sha256hash + except (Machine.DoesNotExist, Inventory.DoesNotExist): + pass + else: + raise Http404 + return HttpResponse(sha256hash) + + +@login_required +def index(request): + # This really should just select on the BU's the user has access to like the + # Main page, but this will do for now + user = request.user + user_level = user.userprofile.level + if user_level != 'GA': + return redirect(index) + inventory = InventoryItem.objects.all() + found = unique_apps(inventory) + + c = {'user': request.user, 'inventory': found, 'page':'front', 'request': request } + return render_to_response('inventory/index.html', c, context_instance=RequestContext(request)) + +@login_required +def bu_inventory(request, bu_id): + user = request.user + user_level = user.userprofile.level + business_unit = get_object_or_404(BusinessUnit, pk=bu_id) + if business_unit not in user.businessunit_set.all() and user_level != 'GA': + print 'not letting you in ' + user_level + return redirect(index) + # Get the groups within the Business Unit + machines = utils.getBUmachines(bu_id) + + inventory = [] + for machine in machines: + for item in machine.inventoryitem_set.all(): + inventory.append(item) + + found = unique_apps(inventory) + c = {'user': request.user, 'inventory': found, 'page':'business_unit', 'business_unit':business_unit, 'request': request} + return render_to_response('inventory/index.html', c, context_instance=RequestContext(request)) + +@login_required +def machine_group_inventory(request, group_id): + user = request.user + user_level = user.userprofile.level + machine_group = get_object_or_404(MachineGroup, pk=group_id) + business_unit = machine_group.business_unit + if business_unit not in user.businessunit_set.all() and user_level != 'GA': + print 'not letting you in ' + user_level + return redirect(index) + + inventory = [] + for machine in machine_group.machine_set.all(): + for item in machine.inventoryitem_set.all(): + inventory.append(item) + + found = unique_apps(inventory) + c = {'user': request.user, 'inventory': found, 'page':'business_unit', 'business_unit':business_unit, 'request': request} + return render_to_response('inventory/index.html', c, context_instance=RequestContext(request)) \ No newline at end of file diff --git a/sal/system_settings.py b/sal/system_settings.py index f54c49e3..83712df3 100644 --- a/sal/system_settings.py +++ b/sal/system_settings.py @@ -180,6 +180,7 @@ 'sal', 'server', 'api', + 'inventory', 'bootstrap3', ) diff --git a/sal/urls.py b/sal/urls.py index c3bba379..38eb3029 100644 --- a/sal/urls.py +++ b/sal/urls.py @@ -18,7 +18,8 @@ # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), - (r'^api/', include('api.urls')) + (r'^api/', include('api.urls')), + (r'^inventory/', include('inventory.urls')) #url(r'^$', 'namer.views.index', name='home'), ) diff --git a/server/migrations/0014_auto_20150817_1646.py b/server/migrations/0014_auto_20150817_1646.py new file mode 100644 index 00000000..370b3afe --- /dev/null +++ b/server/migrations/0014_auto_20150817_1646.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('server', '0013_auto_20150816_1652'), + ] + + operations = [ + migrations.AlterField( + model_name='osqueryresult', + name='unix_time', + field=models.IntegerField(default=1), + preserve_default=False, + ), + ] diff --git a/server/plugins/topprocesses/templates/front.html b/server/plugins/topprocesses/templates/front.html index c211176e..db917d51 100644 --- a/server/plugins/topprocesses/templates/front.html +++ b/server/plugins/topprocesses/templates/front.html @@ -8,14 +8,16 @@
    {% for item in data %} + {% if item.column_data %} + {% endif %} {% endfor %}
    - + {{ item.column_data }} {{ item.data_count }}
    diff --git a/server/plugins/topprocesses/templates/id.html b/server/plugins/topprocesses/templates/id.html index b38e5bbf..aa1c6348 100644 --- a/server/plugins/topprocesses/templates/id.html +++ b/server/plugins/topprocesses/templates/id.html @@ -8,14 +8,16 @@
    {% for item in data %} + {{% if item.column_data %} + {% endif %} {% endfor %}
    - + {{ item.column_data }} {{ item.data_count }}
    diff --git a/server/plugins/topprocesses/topprocesses.py b/server/plugins/topprocesses/topprocesses.py index a35bed6f..8c47dc89 100644 --- a/server/plugins/topprocesses/topprocesses.py +++ b/server/plugins/topprocesses/topprocesses.py @@ -38,7 +38,6 @@ def show_widget(self, page, machines=None, theid=None): info = OSQueryColumn.objects.filter(osquery_result__name='pack_sal_top_processes').filter(osquery_result__machine=machines).filter(column_name='name').values('column_data').annotate(data_count=Count('column_data')).order_by('-data_count')[:100:1] else: info = [] - c = Context({ 'title': 'Top Processes', 'data': info, @@ -51,6 +50,6 @@ def filter_machines(self, machines, data): # You will be passed a QuerySet of machines, you then need to perform some filtering based on the 'data' part of the url from the show_widget output. Just return your filtered list of machines and the page title. machines = machines.filter(osquery_results__osquery_columns__column_data__exact=data).filter(osquery_results__name__exact='pack_sal_top_processes').distinct() - print machines + return machines, 'Machines running '+data \ No newline at end of file diff --git a/server/templates/server/bu_dashboard.html b/server/templates/server/bu_dashboard.html index 53517861..4e977cfc 100755 --- a/server/templates/server/bu_dashboard.html +++ b/server/templates/server/bu_dashboard.html @@ -10,6 +10,7 @@ {% if user.userprofile.level == 'GA' %}
  • Back
  • {% endif %} +
  • Application Inventory
  • {{business_unit}}