diff --git a/README.md b/README.md
index 90fcd4a..0e99e97 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,12 @@ BlueHound supports presenting your data as tables, graphs, bar charts, line char
4. **Easy Customization**: Any custom collection method can be added into BlueHound. Users can even add their own custom parameters or even custom icons for their graphs.
## Getting Started
+### ROST ISO
+BlueHound can be used as part of the [ROST image](https://zeronetworks.com/ROST-iso.zip), which comes pre-configured with everything you need (BlueHound, Neo4j, BloodHound, and a sample dataset).
+
To load ROST, create a new virtual machine, and install it from the ISO like you would for a new Windows host.
+### BlueHound Binary
+If you already have a Neo4j instance running, you can download a pre-compiled version of BlueHound from our [release page](https://github.com/zeronetworks/BlueHound/releases). Just download the zip file suitable to your OS version, extract it, and run the binary.
+### Using BlueHound
1. Connect to your Neo4j server
2. Download [SharpHound](https://github.com/BloodHoundAD/BloodHound/blob/master/Collectors/SharpHound.exe), [ShotHound](https://github.com/zeronetworks/BloodHound-Tools/tree/main/ShotHound) and the [Vulnerability Scanner report parser](https://github.com/zeronetworks/BloodHound-Tools/tree/main/VulnerabilitiesDataImport)
3. Use the **Data Import** section to collect & import data into your Neo4j database.
diff --git a/package.json b/package.json
index 3493a6e..ebb0176 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "bluehound",
"productName": "BlueHound",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "BlueHound",
"neo4jDesktop": {
"apiVersion": "^1.2.0"
@@ -88,7 +88,8 @@
"async-mutex": "^0.3.2",
"jszip": "^3.10.0",
"file-saver": "^2.0.5",
- "react-lottie-player": "^1.4.3"
+ "react-lottie-player": "^1.4.3",
+ "dayjs": "^1.11.5"
},
"devDependencies": {
"@babel/cli": "^7.16.8",
diff --git a/public/zero-networks-logo.png b/public/zero-networks-logo.png
new file mode 100644
index 0000000..5a098bf
Binary files /dev/null and b/public/zero-networks-logo.png differ
diff --git a/public/zero-networks-logo.svg b/public/zero-networks-logo.svg
new file mode 100644
index 0000000..19e0e7b
--- /dev/null
+++ b/public/zero-networks-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/application/Application.tsx b/src/application/Application.tsx
index bfb9e18..a3b2fc2 100644
--- a/src/application/Application.tsx
+++ b/src/application/Application.tsx
@@ -101,7 +101,9 @@ const Application = ({ connection, connected, hasCachedDashboard, oldDashboard,
createConnection(connection.protocol, connection.url, connection.port, connection.database, connection.username, connection.password);
}
-
+ if (window.electron != undefined) { // running in Electron
+ window.electron.appLoaded();
+ }
}
// Only render the dashboard component if we have an active Neo4j connection.
diff --git a/src/application/ApplicationActions.tsx b/src/application/ApplicationActions.tsx
index 47fb890..bdf4882 100644
--- a/src/application/ApplicationActions.tsx
+++ b/src/application/ApplicationActions.tsx
@@ -125,6 +125,12 @@ export const setParallelQueries = (amount: number) => ({
payload: { amount },
});
+export const SET_COLLECTION_HOURS = 'APPLICATION/SET_COLLECTION_HOURS';
+export const setCollectionHours = (amount: number) => ({
+ type: SET_COLLECTION_HOURS,
+ payload: { amount },
+});
+
export const SET_SHARPHOUND_UPLOAD_RESULTS = 'APPLICATION/SET_SHARPHOUND_UPLOAD_RESULTS';
export const setSharpHoundUploadResults = (status: boolean) => ({
type: SET_SHARPHOUND_UPLOAD_RESULTS,
@@ -191,4 +197,16 @@ export const SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING = 'APPLICATION/SET_DASHBOARD
export const setDashboardToLoadAfterConnecting = (id: any) => ({
type: SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING,
payload: { id },
+});
+
+export const SET_ZERO_ALERT_SHOWN = 'APPLICATION/SET_ZERO_ALERT_SHOWN';
+export const setZeroAlertShown = (alert_state: boolean) => ({
+ type: SET_ZERO_ALERT_SHOWN,
+ payload: { alert_state}
+});
+
+export const SET_ZERO_ALERT_OPEN = 'APPLICATION/SET_ZERO_ALERT_OPEN';
+export const setZeroAlertOpen = (alert_state: boolean) => ({
+ type: SET_ZERO_ALERT_OPEN,
+ payload: { alert_state}
});
\ No newline at end of file
diff --git a/src/application/ApplicationReducer.tsx b/src/application/ApplicationReducer.tsx
index 34e1f52..c2bea68 100644
--- a/src/application/ApplicationReducer.tsx
+++ b/src/application/ApplicationReducer.tsx
@@ -13,6 +13,7 @@ import {
SET_CONNECTION_MODAL_OPEN,
SET_CONNECTION_PROPERTIES,
SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING,
+ SET_ZERO_ALERT_SHOWN,
SET_DESKTOP_CONNECTION_PROPERTIES,
SET_OLD_DASHBOARD,
SET_SHARE_DETAILS_FROM_URL,
@@ -31,7 +32,12 @@ import {
SET_SHARPHOUND_UPLOAD_RESULTS,
SET_SHARPHOUND_CLEAR_RESULTS,
SET_FILTER_MODAL_OPEN,
- SET_QUERY_MODAL_OPEN, SET_PARALLEL_QUERIES, SET_DB_VERSION, SET_CACHED_QUERIES
+ SET_QUERY_MODAL_OPEN,
+ SET_PARALLEL_QUERIES,
+ SET_DB_VERSION,
+ SET_CACHED_QUERIES,
+ SET_ZERO_ALERT_OPEN,
+ SET_COLLECTION_HOURS
} from "./ApplicationActions";
const update = (state, mutations) =>
@@ -63,9 +69,12 @@ const initialState =
standalone: false,
toolsParallel: false,
parallelQueries: 5,
+ collectionHours: 0,
sharphoundUploadResults: true,
sharphoundClearResults: true,
- cachedQueries: {}
+ cachedQueries: {},
+ zeroAlertShown: true,
+ zeroAlertOpen: false
}
export const applicationReducer = (state = initialState, action: { type: any; payload: any; }) => {
@@ -151,6 +160,11 @@ export const applicationReducer = (state = initialState, action: { type: any; pa
state = update(state, { parallelQueries: amount })
return state
}
+ case SET_COLLECTION_HOURS: {
+ const { amount } = payload;
+ state = update(state, { collectionHours: amount })
+ return state
+ }
case SET_SHARPHOUND_UPLOAD_RESULTS: {
const { status } = payload;
state = update(state, { sharphoundUploadResults: status })
@@ -211,6 +225,16 @@ export const applicationReducer = (state = initialState, action: { type: any; pa
state = update(state, { dashboardToLoadAfterConnecting: id })
return state;
}
+ case SET_ZERO_ALERT_SHOWN: {
+ const { alert_state } = payload;
+ state = update(state, { zeroAlertShown: alert_state })
+ return state;
+ }
+ case SET_ZERO_ALERT_OPEN: {
+ const { alert_state } = payload;
+ state = update(state, { zeroAlertOpen: alert_state })
+ return state;
+ }
case SET_CONNECTION_PROPERTIES: {
const { protocol, url, port, database, username, password, successful } = payload;
state = update(state, {
diff --git a/src/application/ApplicationSelectors.tsx b/src/application/ApplicationSelectors.tsx
index 02391ee..52f7fdc 100644
--- a/src/application/ApplicationSelectors.tsx
+++ b/src/application/ApplicationSelectors.tsx
@@ -121,10 +121,22 @@ export const getParallelQueries = (state: any) => {
return state.application.parallelQueries;
}
+export const getCollectionHours = (state: any) => {
+ return state.application.collectionHours;
+}
+
export const getSharpHoundUploadResults = (state: any) => {
return state.application.sharphoundUploadResults;
}
export const getSharpHoundClearResults = (state: any) => {
return state.application.sharphoundClearResults;
+}
+
+export const getZeroAlertShown = (state: any) => {
+ return state.application.zeroAlertShown;
+}
+
+export const getZeroAlertOpen = (state: any) => {
+ return state.application.zeroAlertOpen;
}
\ No newline at end of file
diff --git a/src/chart/GraphChart.tsx b/src/chart/GraphChart.tsx
index 7c08691..f45db33 100644
--- a/src/chart/GraphChart.tsx
+++ b/src/chart/GraphChart.tsx
@@ -23,10 +23,11 @@ const getNodeLabel = (labels) => {
for (let bloodhoundLabel of BLOODHOUND_NODE_LABELS) {
const foundIndex = labels.indexOf(bloodhoundLabel);
if (foundIndex != -1) {
- return labels[foundIndex]
+ return labels[foundIndex];
}
}
- return labels[-1]
+
+ return labels[labels.length-1];
}
const update = (state, mutations) =>
@@ -283,6 +284,7 @@ const NeoGraphChart = (props: ChartProps) => {
const newImage = new Image();
newImage.src = node.lastLabel + '.png';
if (imageExists(newImage)) {
+ console.log(newImage.src)
imgs[node.lastLabel] = newImage;
image = newImage;
}
diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx
index f4b194b..75f06f1 100644
--- a/src/dashboard/Dashboard.tsx
+++ b/src/dashboard/Dashboard.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, {useCallback, useEffect} from 'react';
import NeoPage from '../page/Page';
import Container from '@material-ui/core/Container';
import NeoDrawer from './DashboardDrawer';
@@ -12,8 +12,6 @@ import { forceRefreshPage } from '../page/PageActions';
import { getPageNumber } from '../settings/SettingsSelectors';
import { createNotification } from '../application/ApplicationActions';
-
-
const Dashboard = ({ pagenumber, connection, onConnectionUpdate }) => {
const [open, setOpen] = React.useState(false);
const driver = createDriver(connection.protocol, connection.url, connection.port, connection.username, connection.password);
diff --git a/src/main.ts b/src/main.ts
index e8135d4..c90947c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,4 @@
-const {app, BrowserWindow, ipcMain, dialog, shell, Menu } = require('electron');
+const {app, BrowserWindow, ipcMain, dialog, shell, Menu, ipcRenderer } = require('electron');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
@@ -6,6 +6,7 @@ const { handleSharpHoundResultsUpload } = require('./collectors/BloodHoundUpload
const logMessages = [];
let mainWindow;
+const gotTheLock = app.requestSingleInstanceLock();
let shellEnvironments;
let logLevel = {
0: "verbose",
@@ -57,10 +58,45 @@ function createWindow () {
});
}
-app.on('ready', createWindow);
+if (!gotTheLock) {
+ app.quit();
+} else {
+ app.on('second-instance', (event, commandLine, workingDirectory, additionalData) => {
+ if (commandLine.includes('--collection-only')) {
+ mainWindow.webContents.send('run-all-collection');
+ };
+
+ if (mainWindow) {
+ if (mainWindow.isMinimized()) mainWindow.restore()
+ mainWindow.focus()
+ }
+ })
+
+ app.whenReady().then(() => {
+ createWindow();
+ })
+}
+
+//app.on('ready', createWindow);
const runningProcesses = {};
+const isCollectionOnly = () => {
+ return process.argv.includes('--collection-only');
+}
+
+ipcMain.handle("app-loaded", async (event) => {
+ if (isCollectionOnly()) {
+ dialog.showMessageBox(mainWindow, {
+ title: 'Collection-only mode',
+ buttons: ['Dismiss'],
+ type: 'info',
+ message: 'BlueHound is running in collection-only mode…',
+ });
+ event.sender.send('run-all-collection', '');
+ }
+});
+
ipcMain.handle("open-dev-tools", async (event) => {
await mainWindow.webContents.openDevTools();
});
@@ -154,7 +190,7 @@ const runToolsInSerial = async (event, toolsData) => {
result.on('close', (code) => {
event.sender.send("tool-data-done", tool.toolId, code, path.dirname(toolPath))
delete runningProcesses[tool.toolId];
- if (toolsData.length > 1) { runToolsInSerial(event, toolsData.slice(1)) };
+ if (toolsData.length > 1) { runToolsInSerial(event, toolsData.slice(1)) } ;
});
}
@@ -195,4 +231,47 @@ ipcMain.handle("kill-process", async (event, toolId) => {
ipcMain.handle("upload-sharphound-results", async (event, toolId, resultsPath, connectionProperties, clearResults) => {
await handleSharpHoundResultsUpload(event, toolId, resultsPath, connectionProperties, clearResults);
+})
+
+ipcMain.handle("tools-finished-running", async (event) => {
+ if (isCollectionOnly()) {
+ app.quit();
+ }
+})
+
+ipcMain.handle("add-scheduled-task", async (event, scheduleFrequency, dayOfWeek, dayOfMonth, scheduleTime) => {
+ if (os.platform() != 'win32') {
+ dialog.showMessageBox(mainWindow, {
+ title: 'OS not supported',
+ buttons: ['Dismiss'],
+ type: 'error',
+ message: 'Adding scheduled task is currently only supported on Windows.\n\n' +
+ 'The --collection-only argument can be used to manually schedule BlueHound on non-Windows hosts.',
+ });
+ return;
+ }
+
+ let args = [];
+ const taskName = '"BlueHound Collection"';
+ const appPathWithArgs = `"'${path.resolve('.', 'BlueHound.exe')}' --collection-only"`;
+
+ if (scheduleFrequency == 'DAILY') {
+ args = ['/CREATE', '/F', '/SC DAILY', '/TN ' + taskName, '/TR ' + appPathWithArgs, '/ST ' + scheduleTime];
+ } else if (scheduleFrequency == 'WEEKLY') {
+ args = ['/CREATE', '/F', '/SC WEEKLY', '/D ' + dayOfWeek, '/TN ' + taskName, '/TR ' + appPathWithArgs, '/ST ' + scheduleTime];
+ } else if (scheduleFrequency == 'MONTHLY') {
+ args = ['/CREATE', '/F', '/SC MONTHLY', '/D ' + dayOfMonth, '/TN ' + taskName, '/TR ' + appPathWithArgs, '/ST ' + scheduleTime];
+ }
+
+ const result = spawn("SCHTASKS", args, { shell: true });
+
+ result.on('error', function (err) { // needed for catching ENOENT
+ event.sender.send("tool-notification", err)
+ });
+
+ result.on('close', (code) => {
+ if (code == 0) {
+ event.sender.send("tool-notification", "Scheduled task added successfully.")
+ }
+ });
})
\ No newline at end of file
diff --git a/src/modal/AboutModal.tsx b/src/modal/AboutModal.tsx
index cbfdd61..36c9238 100644
--- a/src/modal/AboutModal.tsx
+++ b/src/modal/AboutModal.tsx
@@ -16,7 +16,7 @@ import {applicationGetDebugState} from "../application/ApplicationSelectors";
import JSZip from 'jszip';
import FileSaver from 'file-saver';
-const version = "1.0.0";
+const version = "1.1.0";
if (window.electron != undefined) {
window.electron.receive("console-messages", (consoleData) => {
@@ -142,35 +142,32 @@ export const NeoAboutModal = ({ open, handleClose, getDebugState }) => {
so if you have ideas, don’t hesitate to reach out to us at {email} or via the Github repository.
- }> - Debug Report - - - | -- v{version} - | -