From 94c9a4cb337f51dc17e88585c9a0ef4448324d4d Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Wed, 7 Nov 2018 20:54:51 -0500
Subject: [PATCH 01/13] Use requests to download assets

---
 .travis.yml   |  2 +-
 brunnhilde.py | 37 +++++++++++++++++++++++++++----------
 2 files changed, 28 insertions(+), 11 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 4705995..544847a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,7 +12,7 @@ before_install:
   - cd sleuthkit && ./bootstrap && ./configure && make && sudo make install && sudo ldconfig
   - cd ..
 install:
-  - pip install wget
+  - pip install requests
 script:
   - python test.py
 
diff --git a/brunnhilde.py b/brunnhilde.py
index 67b02a1..c540aaf 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -26,11 +26,11 @@
 import math
 import os
 import re
+import requests
 import shutil
 import sqlite3
 import subprocess
 import sys
-import wget
 
 def run_siegfried(args, source_dir, use_hash):
     """Run siegfried on directory"""
@@ -594,6 +594,12 @@ def write_pronom_links(old_file, new_file):
     in_file.close()
     out_file.close()
 
+def download_asset_file(asset_url, asset_filepath):
+    """Download file from asset_url and write to asset_filepath"""
+    r = requests.get(asset_url)
+    with open(asset_filepath, "wb") as f:
+        f.write(r.content)
+
 def close_files_conns_on_exit(html, conn, cursor, report_dir):
     cursor.close()
     conn.close()
@@ -691,17 +697,28 @@ def main():
         os.makedirs(newdir)
 
     # download assets
-    bootstrap_css_url = 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/css/bootstrap.min.css'
-    bootstrap_js_url = 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/bootstrap.min.js'
-    jquery_url = 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/jquery-3.3.1.slim.min.js'
-    popper_url = 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/popper.min.js'
-
+    assets_to_download = [
+        {
+            'filepath': os.path.join(css, 'bootstrap.min.css'),
+            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/css/bootstrap.min.css'
+        },
+        {
+            'filepath': os.path.join(js, 'bootstrap.min.js'),
+            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/bootstrap.min.js'
+        },
+        {
+            'filepath': os.path.join(js, 'jquery-3.3.1.slim.min.js'),
+            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/jquery-3.3.1.slim.min.js'
+        },
+        {
+            'filepath': os.path.join(js, 'popper.min.js'),
+            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/popper.min.js'
+        }
+    ]
     print("\nDownloading CSS and JS files from Github...")
     try:
-        wget.download(bootstrap_css_url, os.path.join(css, 'bootstrap.min.css'))
-        wget.download(bootstrap_js_url, os.path.join(js, 'bootstrap.min.js'))
-        wget.download(jquery_url, os.path.join(js, 'jquery-3.3.1.slim.min.js'))
-        wget.download(popper_url, os.path.join(js, 'popper.min.js'))
+        for a in assets_to_download:
+            download_asset_file(a['url'], a['filepath'])
         print("\nDownloads complete.")
     except Exception:
         print("Unable to download required CSS and JS files. Please ensure your internet connection is working and try again.")

From ae4415a61f977317eb10f85e1dfd4978929aba63 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Wed, 7 Nov 2018 20:55:28 -0500
Subject: [PATCH 02/13] Consistent line spacing in terminal

---
 brunnhilde.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/brunnhilde.py b/brunnhilde.py
index c540aaf..9af2896 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -721,7 +721,7 @@ def main():
             download_asset_file(a['url'], a['filepath'])
         print("\nDownloads complete.")
     except Exception:
-        print("Unable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
+        print("\nUnable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
         sys.exit(1)
 
     # create html report

From 650c8d9234b0798b09dd4b6489a89dc9667a7f30 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Wed, 7 Nov 2018 20:57:35 -0500
Subject: [PATCH 03/13] More clearly indicate error message

---
 brunnhilde.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/brunnhilde.py b/brunnhilde.py
index 9af2896..a6670ce 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -721,7 +721,7 @@ def main():
             download_asset_file(a['url'], a['filepath'])
         print("\nDownloads complete.")
     except Exception:
-        print("\nUnable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
+        print("\nERROR: Unable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
         sys.exit(1)
 
     # create html report

From 91da0cc31345bafe029a369d20a3213d477324db Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Wed, 7 Nov 2018 21:00:36 -0500
Subject: [PATCH 04/13] Hidden .assets directory

---
 brunnhilde.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/brunnhilde.py b/brunnhilde.py
index a6670ce..e65f7dd 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -259,7 +259,7 @@ def get_stats(args, source_dir, scan_started, cursor, html, brunnhilde_version,
     html.write('\n<head>')
     html.write('\n<title>Brunnhilde report: %s</title>' % basename)
     html.write('\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">')
-    html.write('\n<link rel="stylesheet" href="./assets/css/bootstrap.min.css">')
+    html.write('\n<link rel="stylesheet" href="./.assets/css/bootstrap.min.css">')
     html.write('\n</head>')
     html.write('\n<body style="padding-top: 80px">')
     # navbar
@@ -547,9 +547,9 @@ def close_html(html):
     html.write('\n</div>')
     html.write('\n</div>')
     html.write('\n</div>')
-    html.write('\n<script src="./assets/js/jquery-3.3.1.slim.min.js"></script>')
-    html.write('\n<script src="./assets/js/popper.min.js"></script>')
-    html.write('\n<script src="./assets/js/bootstrap.min.js"></script>')
+    html.write('\n<script src="./.assets/js/jquery-3.3.1.slim.min.js"></script>')
+    html.write('\n<script src="./.assets/js/popper.min.js"></script>')
+    html.write('\n<script src="./.assets/js/bootstrap.min.js"></script>')
     html.write('\n<script>$(".navbar-nav .nav-link").on("click", function(){ $(".navbar-nav").find(".active").removeClass("active"); $(this).addClass("active"); });</script>')
     html.write('\n<script>$(".navbar-brand").on("click", function(){ $(".navbar-nav").find(".active").removeClass("active"); });</script>')
     html.write('\n</body>')
@@ -688,7 +688,7 @@ def main():
                 raise
 
     # create assets dirs
-    assets_target = os.path.join(report_dir, 'assets')
+    assets_target = os.path.join(report_dir, '.assets')
     if os.path.exists(assets_target):
         shutil.rmtree(assets_target)
     css = os.path.join(assets_target, 'css')

From d06729c37aec4ebf3f80e3a260ead22e5e1c95a8 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Wed, 7 Nov 2018 21:10:58 -0500
Subject: [PATCH 05/13] Update wget -> requests

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 65e8fa9..740c811 100644
--- a/README.md
+++ b/README.md
@@ -194,7 +194,7 @@ For Brunnhilde to report on any directory of content, the following must be inst
 
 * Python (2.7 or 3.4+; Python 3 is recommended)
 * [Siegfried](http://www.itforarchivists.com/siegfried): Brunnhilde is now compatible with all version of Siegfried, including 1.6+. It does not support MIME-Info or FDD signatures: for Brunnhilde to work, Siegfried must be using the PRONOM signature file only. If you have been using MIME-Info or FDD signatures as a replacement for or alongside PRONOM with Siegfried 1.5/1.6 on your machine, entering `roy build -multi 0` in the terminal should return you to Siegfried's default PRONOM-only identification mode and allow Brunnhilde to work properly.  
-* [wget Python module](https://pypi.org/project/wget/): For downloading CSS and JS files for HTML report from this repository. This should be automatically installed as a dependency when Brunnhilde is installed via pip.
+* [requests Python module](https://pypi.org/project/requests/): For downloading CSS and JS files for HTML report from this repository. This should be automatically installed as a dependency when Brunnhilde is installed via pip.
 
 #### Additional dependencies (for full functionality in Linux and macOS)
 

From dd41ec23524c6eee6e0503e9039094a3bf0a4467 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Wed, 7 Nov 2018 21:17:48 -0500
Subject: [PATCH 06/13] Bump version to 1.8.0 and install requests on pip
 install

---
 README.md     | 2 +-
 brunnhilde.py | 2 +-
 setup.py      | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 740c811..917a789 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 ## Brunnhilde - A reporting companion to Siegfried  
 
-### Version: Brunnhilde 1.7.2
+### Version: Brunnhilde 1.8.0
 
 [![Build Status](https://travis-ci.org/timothyryanwalsh/brunnhilde.svg?branch=master)](https://travis-ci.org/timothyryanwalsh/brunnhilde)
 
diff --git a/brunnhilde.py b/brunnhilde.py
index e65f7dd..7d5b23f 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -635,7 +635,7 @@ def _make_parser(version):
 
 def main():
     # system info
-    brunnhilde_version = 'brunnhilde 1.7.2'
+    brunnhilde_version = 'brunnhilde 1.8.0'
     siegfried_version = subprocess.check_output(["sf", "-version"]).decode()
 
     parser = _make_parser(brunnhilde_version)
diff --git a/setup.py b/setup.py
index 6a3399d..b210690 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 setup(
     name = 'brunnhilde',
-    version = '1.7.2',
+    version = '1.8.0',
     url = 'https://github.com/timothyryanwalsh/brunnhilde',
     author = 'Tim Walsh',
     author_email = 'timothyryanwalsh@gmail.com',
@@ -11,7 +11,7 @@
     description = 'A Siegfried-based digital archives reporting tool for directories and disk images',
     keywords = 'archives reporting characterization identification diskimages',
     platforms = ['POSIX', 'Windows'],
-    install_requires=['wget'],
+    install_requires=['requests'],
     test_suite='test',
     classifiers = [
         'Development Status :: 5 - Production/Stable',

From 1e27dc032d950c46f403e891a6bf85b1ebc73521 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Thu, 8 Nov 2018 15:11:36 -0500
Subject: [PATCH 07/13] Add --save_assets and --load_assets arguments

---
 README.md     | 20 +++++++++++-
 brunnhilde.py | 86 ++++++++++++++++++++++++++++++++++-----------------
 2 files changed, 77 insertions(+), 29 deletions(-)

diff --git a/README.md b/README.md
index 917a789..44e45c9 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,8 @@ usage: brunnhilde.py [-h] [-a] [-b] [--ssn_mode SSN_MODE] [-d] [--hfs]
                      [--resforks] [--tsk_imgtype TSK_IMGTYPE]
                      [--tsk_fstype TSK_FSTYPE]
                      [--tsk_sector_offset TSK_SECTOR_OFFSET] [--hash HASH]
-                     [--largefiles] [-n] [-r] [-t] [-V] [-w] [-z]
+                     [-k] [-l] [-n] [-r] [-t] [-V] [-w] [-z]
+                     [--save_assets SAVE_ASSETS] [--load_assets LOAD_ASSETS]
                      source destination basename
 
 positional arguments:
@@ -94,6 +95,14 @@ optional arguments:
   -w, --showwarnings    Add Siegfried warnings to HTML report
   -z, --scanarchives    Decompress and scan zip, tar, gzip, warc, arc with
                         Siegfried
+  --save_assets SAVE_ASSETS
+                        Specify filepath location to save JS/CSS files for use
+                        in subsequent runs (this directory should not yet
+                        exist)
+  --load_assets LOAD_ASSETS
+                        Specify filepath location of JS/CSS files to copy to
+                        destination (instead of downloading)
+
 
 ```  
   
@@ -188,6 +197,15 @@ To extract AppleDouble resource forks from HFS-formatted disk images, pass the `
 
 All dependencies are already installed in BitCurator 1.7.106+. See instructions below for installing dependencies if you wish to use Brunnhilde in a different environment (Linux, Mac, or Windows).
 
+#### Internet connection
+
+In order to ensure that the CSS and JavaScript files needed for the Brunnhilde HTML report are included with the report and thus not a preservation risk themselves, these files are downloaded from this Github repository every time Brunnhilde runs.
+
+If you want to run Brunnhilde without an internet connection:
+
+* The first time you run Brunnhilde, use the `save_assets` argument to specify a directory to which Brunnhilde can copy the CSS and JS assets needed for the report. This can be a relative or absolute path. Ideally this path should be memorable and should not yet exist.
+* In subsequent runs, use the `load_assets` argument to specify a directory from which Brunnhilde can copy the CSS and JS assets rather than downloading them from Github. This removes the need for an internet connection when running Brunnhilde after the first time.
+
 #### Core requirements (all operating systems)  
 
 For Brunnhilde to report on any directory of content, the following must be installed in addition to Brunnhilde:
diff --git a/brunnhilde.py b/brunnhilde.py
index 7d5b23f..2a08d61 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -606,7 +606,6 @@ def close_files_conns_on_exit(html, conn, cursor, report_dir):
     html.close()
     shutil.rmtree(report_dir)
 
-
 def _make_parser(version):
     parser = argparse.ArgumentParser()
     parser.add_argument("-a", "--allocated", help="Instruct tsk_recover to export only allocated files (recovers all files by default)", action="store_true")
@@ -627,6 +626,8 @@ def _make_parser(version):
     parser.add_argument("-V", "--version", help="Display Brunnhilde version", action="version", version="%s" % version)
     parser.add_argument("-w", "--showwarnings", help="Add Siegfried warnings to HTML report", action="store_true")
     parser.add_argument("-z", "--scanarchives", help="Decompress and scan zip, tar, gzip, warc, arc with Siegfried", action="store_true")
+    parser.add_argument("--save_assets", help="Specify filepath location to save JS/CSS files for use in subsequent runs (this directory should not yet exist)", action="store")
+    parser.add_argument("--load_assets", help="Specify filepath location of JS/CSS files to copy to destination (instead of downloading)", action="store")
     parser.add_argument("source", help="Path to source directory or disk image")
     parser.add_argument("destination", help="Path to destination for reports")
     parser.add_argument("basename", help="Accession number or identifier, used as basename for outputs")
@@ -696,33 +697,62 @@ def main():
     for newdir in assets_target, css, js:
         os.makedirs(newdir)
 
-    # download assets
-    assets_to_download = [
-        {
-            'filepath': os.path.join(css, 'bootstrap.min.css'),
-            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/css/bootstrap.min.css'
-        },
-        {
-            'filepath': os.path.join(js, 'bootstrap.min.js'),
-            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/bootstrap.min.js'
-        },
-        {
-            'filepath': os.path.join(js, 'jquery-3.3.1.slim.min.js'),
-            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/jquery-3.3.1.slim.min.js'
-        },
-        {
-            'filepath': os.path.join(js, 'popper.min.js'),
-            'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/popper.min.js'
-        }
-    ]
-    print("\nDownloading CSS and JS files from Github...")
-    try:
-        for a in assets_to_download:
-            download_asset_file(a['url'], a['filepath'])
-        print("\nDownloads complete.")
-    except Exception:
-        print("\nERROR: Unable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
-        sys.exit(1)
+    # use local copies of JS/CSS assets if path specified by user
+    if args.load_assets:
+        src = os.path.join(os.path.abspath(args.load_assets), 'brunnhilde_assets')
+        # delete directory if already exists
+        if os.path.exists(assets_target):
+            shutil.rmtree(assets_target)
+        # copy
+        try:
+            shutil.copytree(src, assets_target)
+            print('\nBrunnhilde CSS and JS assets successfully copied to destination from "%s".' % (os.path.abspath(args.load_assets)))
+        except (shutil.Error, OSError) as e:
+            print("\nERROR: Unable to copy assets from --load_assets path. Detailed output: %s" % (e))
+            sys.exit(1)
+
+    # otherwise, download from github
+    else:
+        assets_to_download = [
+            {
+                'filepath': os.path.join(css, 'bootstrap.min.css'),
+                'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/css/bootstrap.min.css'
+            },
+            {
+                'filepath': os.path.join(js, 'bootstrap.min.js'),
+                'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/bootstrap.min.js'
+            },
+            {
+                'filepath': os.path.join(js, 'jquery-3.3.1.slim.min.js'),
+                'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/jquery-3.3.1.slim.min.js'
+            },
+            {
+                'filepath': os.path.join(js, 'popper.min.js'),
+                'url': 'https://github.com/timothyryanwalsh/brunnhilde/blob/master/assets/js/popper.min.js'
+            }
+        ]
+        print("\nDownloading CSS and JS files from Github...")
+        try:
+            for a in assets_to_download:
+                download_asset_file(a['url'], a['filepath'])
+            print("\nDownloads complete.")
+        except Exception:
+            print("\nERROR: Unable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
+            sys.exit(1)
+
+        # save a copy locally if option is selected by user
+        if args.save_assets:
+            user_path = os.path.abspath(args.save_assets)
+            new_dir = os.path.join(user_path, 'brunnhilde_assets')
+            # overwrite if exists
+            if os.path.exists(new_dir):
+                shutil.rmtree(new_dir)
+            # copy
+            try:
+                shutil.copytree(assets_target, new_dir)
+                print('\nBrunnhilde CSS and JS assets saved locally. To use these in subsequent runs rather than downloading the files from Github, use this argument: --load_assets "%s"' % (user_path))
+            except shutil.Error as e:
+                print("\nERROR: Unable to copy CSS and JS assets to --save_assets path. Detailed output: %s" % (e))         
 
     # create html report
     temp_html = os.path.join(report_dir, 'temp.html')

From 7bc506df9fe82f5a81ac87895a2a39ecccec3cf3 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Thu, 8 Nov 2018 15:16:24 -0500
Subject: [PATCH 08/13] Update README

---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 44e45c9..e80b68d 100644
--- a/README.md
+++ b/README.md
@@ -199,12 +199,12 @@ All dependencies are already installed in BitCurator 1.7.106+. See instructions
 
 #### Internet connection
 
-In order to ensure that the CSS and JavaScript files needed for the Brunnhilde HTML report are included with the report and thus not a preservation risk themselves, these files are downloaded from this Github repository every time Brunnhilde runs.
+In order to ensure that the CSS and JavaScript files needed for the Brunnhilde HTML report are included with the report and thus not a preservation risk, these assets are downloaded from this Github repository every time Brunnhilde runs.
 
 If you want to run Brunnhilde without an internet connection:
 
-* The first time you run Brunnhilde, use the `save_assets` argument to specify a directory to which Brunnhilde can copy the CSS and JS assets needed for the report. This can be a relative or absolute path. Ideally this path should be memorable and should not yet exist.
-* In subsequent runs, use the `load_assets` argument to specify a directory from which Brunnhilde can copy the CSS and JS assets rather than downloading them from Github. This removes the need for an internet connection when running Brunnhilde after the first time.
+* The first time you run Brunnhilde, use the `--save_assets` argument to specify a directory to which Brunnhilde can copy the CSS and JS assets needed for the report. This can be a relative or absolute path. Ideally this path should be memorable and should not yet exist.
+* In subsequent runs, use the `--load_assets` argument to specify a directory from which Brunnhilde can copy the CSS and JS assets rather than downloading them from Github. This removes the need for an internet connection when running Brunnhilde after the first time.
 
 #### Core requirements (all operating systems)  
 

From c496f14aa00d729485ba2cc803d037bd4cefa822 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Thu, 8 Nov 2018 15:18:15 -0500
Subject: [PATCH 09/13] Update internet connection error message

---
 brunnhilde.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/brunnhilde.py b/brunnhilde.py
index 2a08d61..606c45c 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -737,7 +737,7 @@ def main():
                 download_asset_file(a['url'], a['filepath'])
             print("\nDownloads complete.")
         except Exception:
-            print("\nERROR: Unable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
+            print("\nERROR: Unable to download required CSS and JS files. Please ensure your internet connection is working and try again or specify the path to where files can be copied from locally with the --load_assets argument.")
             sys.exit(1)
 
         # save a copy locally if option is selected by user

From c241bb5f9766d3a114ca505c457b965727ded6c0 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Thu, 8 Nov 2018 15:35:13 -0500
Subject: [PATCH 10/13] Fix and improve SSN reporting

---
 brunnhilde.py | 39 ++++++++++++++++++---------------------
 1 file changed, 18 insertions(+), 21 deletions(-)

diff --git a/brunnhilde.py b/brunnhilde.py
index 606c45c..6b7c5bf 100644
--- a/brunnhilde.py
+++ b/brunnhilde.py
@@ -283,7 +283,7 @@ def get_stats(args, source_dir, scan_started, cursor, html, brunnhilde_version,
     if use_hash == True:
         html.write('\n<a class="nav-item nav-link" href="#Duplicates">Duplicates</a>')
     if args.bulkextractor == True:
-        html.write('\n<a class="nav-item nav-link" href="#PII">PII</a>')
+        html.write('\n<a class="nav-item nav-link" href="#SSNs">SSNs</a>')
     html.write('\n</div>')
     html.write('\n</div>')
     html.write('\n</nav>')
@@ -441,18 +441,18 @@ def write_html(header, path, file_delimiter, html):
     html.write('\n<h4>%s</h4>' % header)
     if header == 'Duplicates':
         html.write('\n<p><em>Duplicates are grouped by hash value.</em></p>')
-    elif header == 'Personally Identifiable Information (PII)':
-        html.write('\n<p><em>Potential PII in source, as identified by bulk_extractor.</em></p>')
+    elif header == 'SSNs':
+        html.write('\n<p><em>Potential Social Security Numbers identified by bulk_extractor.</em></p>')
     
     # if writing PII, handle separately
-    if header == 'Personally Identifiable Information (PII)':
+    if header == 'SSNs':
         if numline > 5: # aka more rows than just header
             html.write('\n<table class="table table-sm table-responsive table-hover">')
             #write header
             html.write('\n<thead>')
             html.write('\n<tr>')
             html.write('\n<th>File</th>')
-            html.write('\n<th>Value Found</th>')
+            html.write('\n<th>Feature</th>')
             html.write('\n<th>Context</th>')
             html.write('\n</tr>')
             html.write('\n</thead>')
@@ -560,13 +560,19 @@ def make_tree(source_dir):
     tree_command = 'tree -tDhR "%s" > "%s"' % (source_dir, os.path.join(report_dir, 'tree.txt'))
     subprocess.call(tree_command, shell=True)
 
-def process_content(args, source_dir, cursor, conn, html, brunnhilde_version, siegfried_version, use_hash):
+def process_content(args, source_dir, cursor, conn, html, brunnhilde_version, siegfried_version, use_hash, ssn_mode):
     """Run through main processing flow on specified directory"""
     scan_started = str(datetime.datetime.now()) # get time
     run_siegfried(args, source_dir, use_hash) # run siegfried
     import_csv(cursor, conn, use_hash) # load csv into sqlite db
     get_stats(args, source_dir, scan_started, cursor, html, brunnhilde_version, siegfried_version, use_hash) # get aggregate stats and write to html file
     generate_reports(args, cursor, html, use_hash) # run sql queries, print to html and csv
+    if args.bulkextractor == True: # bulk extractor option is chosen
+        if not sys.platform.startswith('win'): # skip in Windows
+            run_bulkext(source_dir, ssn_mode)
+            write_html('SSNs', '%s' % os.path.join(bulkext_dir, 'pii.txt'), '\t', html)
+        else:
+            print("\nBulk Extractor not supported on Windows. Skipping.")
     close_html(html) # close HTML file tags
     if not sys.platform.startswith('win'):
         make_tree(source_dir) # create tree.txt on mac and linux machines
@@ -706,7 +712,7 @@ def main():
         # copy
         try:
             shutil.copytree(src, assets_target)
-            print('\nBrunnhilde CSS and JS assets successfully copied to destination from "%s".' % (os.path.abspath(args.load_assets)))
+            print('\nAssets successfully copied to destination from "%s".' % (os.path.abspath(args.load_assets)))
         except (shutil.Error, OSError) as e:
             print("\nERROR: Unable to copy assets from --load_assets path. Detailed output: %s" % (e))
             sys.exit(1)
@@ -737,7 +743,7 @@ def main():
                 download_asset_file(a['url'], a['filepath'])
             print("\nDownloads complete.")
         except Exception:
-            print("\nERROR: Unable to download required CSS and JS files. Please ensure your internet connection is working and try again or specify the path to where files can be copied from locally with the --load_assets argument.")
+            print("\nERROR: Unable to download required CSS and JS files. Please ensure your internet connection is working and try again.")
             sys.exit(1)
 
         # save a copy locally if option is selected by user
@@ -750,9 +756,9 @@ def main():
             # copy
             try:
                 shutil.copytree(assets_target, new_dir)
-                print('\nBrunnhilde CSS and JS assets saved locally. To use these in subsequent runs rather than downloading the files from Github, use this argument: --load_assets "%s"' % (user_path))
+                print('\nBrunnhilde assets saved locally. To use these in subsequent runs, use this argument: --load_assets "%s"' % (user_path))
             except shutil.Error as e:
-                print("\nERROR: Unable to copy CSS and JS assets to --save_assets path. Detailed output: %s" % (e))         
+                print("\nERROR: Unable to copy assets to --save_assets path. Detailed output: %s" % (e))         
 
     # create html report
     temp_html = os.path.join(report_dir, 'temp.html')
@@ -850,10 +856,7 @@ def main():
             # skip clamav on Windows
             if not sys.platform.startswith('win'):
                 run_clamav(args, tempdir)
-        process_content(args, tempdir, cursor, conn, html, brunnhilde_version, siegfried_version, use_hash)
-        if args.bulkextractor == True: # bulk extractor option is chosen
-            run_bulkext(tempdir, ssn_mode)
-            write_html('Personally Identifiable Information (PII)', '%s' % os.path.join(bulkext_dir, 'pii.txt'), '\t', html)
+        process_content(args, tempdir, cursor, conn, html, brunnhilde_version, siegfried_version, use_hash, ssn_mode)
         if args.removefiles == True:
             shutil.rmtree(tempdir)
 
@@ -866,13 +869,7 @@ def main():
             # skip clamav on Windows
             if not sys.platform.startswith('win'):
                 run_clamav(args, source)
-        process_content(args, source, cursor, conn, html, brunnhilde_version, siegfried_version, use_hash)
-        if args.bulkextractor == True: # bulk extractor option is chosen
-            if not sys.platform.startswith('win'): # skip in Windows
-                run_bulkext(source, ssn_mode)
-                write_html('Personally Identifiable Information (PII)', '%s' % os.path.join(bulkext_dir, 'pii.txt'), '\t', html)
-            else:
-                print("\nBulk Extractor not supported on Windows. Skipping.")
+        process_content(args, source, cursor, conn, html, brunnhilde_version, siegfried_version, use_hash, ssn_mode)
 
     # close HTML file
     html.close()

From 298f34a8cc2cb78ef99a1aa8b18b79b8be3e9de0 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Thu, 8 Nov 2018 18:24:48 -0500
Subject: [PATCH 11/13] Add integration test for --save_assets and
 --load_assets

---
 test.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/test.py b/test.py
index 7a2fa0b..c75c77c 100644
--- a/test.py
+++ b/test.py
@@ -151,6 +151,15 @@ def test_integration_retain_sqlite_db(self):
         self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'test', 
             'siegfried.sqlite')))
 
+    def test_integration_save_load_assets(self):
+        assets_dir = os.path.join(self.dest_tmpdir, 'assets_test')
+        subprocess.call('python brunnhilde.py --save_assets "%s" ./test-data/files/ "%s" save-test' % (assets_dir, self.dest_tmpdir), 
+            shell=True)
+        subprocess.call('python brunnhilde.py --load_assets "%s" ./test-data/files/ "%s" load-test' % (assets_dir, self.dest_tmpdir), 
+            shell=True)
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'load-test', 
+            'report.html')))
+
 
 if __name__ == '__main__':
     unittest.main()

From efbb39e24e5643d44e821046a0e8524751e5eaa2 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Thu, 8 Nov 2018 18:32:40 -0500
Subject: [PATCH 12/13] Add integration test for --largefiles arg

---
 test.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/test.py b/test.py
index c75c77c..4aea3d5 100644
--- a/test.py
+++ b/test.py
@@ -135,6 +135,16 @@ def test_integration_clamav(self):
         with open(virus_log, 'r') as f:
             self.assertTrue("Infected files: 0" in f.read())
 
+    def test_integration_clamav_largefiles(self):
+        subprocess.call('python brunnhilde.py -l ./test-data/files/ "%s" test' % (self.dest_tmpdir), 
+            shell=True)
+        # virus log correctly written
+        virus_log = j(self.dest_tmpdir, 'test', 'logs', 'viruscheck-log.txt')
+        with open(virus_log, 'r') as f:
+            self.assertTrue("Scanned files: 4" in f.read())
+        with open(virus_log, 'r') as f:
+            self.assertTrue("Infected files: 0" in f.read())
+
     def test_integration_clamav_diskimage(self):
         subprocess.call('python brunnhilde.py -d ./test-data/diskimages/sample-floppy-fat.dd "%s" test' % (self.dest_tmpdir), 
             shell=True)

From be78168a5a0611a33ec00d2eccffca72d88b8c57 Mon Sep 17 00:00:00 2001
From: Tim Walsh <timothyryanwalsh@gmail.com>
Date: Thu, 8 Nov 2018 19:25:44 -0500
Subject: [PATCH 13/13] Add .asset assertions for integration tests

---
 test.py | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/test.py b/test.py
index 4aea3d5..29c54a3 100644
--- a/test.py
+++ b/test.py
@@ -75,6 +75,16 @@ def test_integration_outputs_created(self):
         if not sys.platform.startswith('win'):
             self.assertTrue(os.path.isfile(j(self.dest_tmpdir, 'test', 
                 'tree.txt')))
+        # bootstrap css/js
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'test', 
+            '.assets', 'css', 'bootstrap.min.css')))
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'test', 
+            '.assets', 'js', 'bootstrap.min.js')))
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'test', 
+            '.assets', 'js', 'jquery-3.3.1.slim.min.js')))
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'test', 
+            '.assets', 'js', 'popper.min.js')))
+
 
     def test_integration_outputs_created_diskimage(self):
         subprocess.call('python brunnhilde.py -nd ./test-data/diskimages/sample-floppy-fat.dd "%s" test' % (self.dest_tmpdir), 
@@ -167,8 +177,18 @@ def test_integration_save_load_assets(self):
             shell=True)
         subprocess.call('python brunnhilde.py --load_assets "%s" ./test-data/files/ "%s" load-test' % (assets_dir, self.dest_tmpdir), 
             shell=True)
+        # report
         self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'load-test', 
             'report.html')))
+        # bootstrap css/js
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'load-test', 
+            '.assets', 'css', 'bootstrap.min.css')))
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'load-test', 
+            '.assets', 'js', 'bootstrap.min.js')))
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'load-test', 
+            '.assets', 'js', 'jquery-3.3.1.slim.min.js')))
+        self.assertTrue(is_non_zero_file(j(self.dest_tmpdir, 'load-test', 
+            '.assets', 'js', 'popper.min.js')))
 
 
 if __name__ == '__main__':