New dev environment

master
Rohan Vazarkar 2016-07-26 19:29:06 -04:00
parent 4db75970ff
commit 55c2003a39
80 changed files with 39675 additions and 15901 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
This is a version of PowerSploit's [PowerView](https://github.com/PowerShellMafia/PowerSploit/blob/master/Recon/PowerView.ps1) customized for BloodHound data collection and ingestion.
It will be kept as a separate instance from PowerView and not kept in sync with the PowerSploit branch.
## BloodHound Functions:
Export-BloodHoundData - Exports PowerView object data to a BloodHound RESTapi instance
Get-BloodHoundData - Queries for machines in the current domain with Get-NetComputer
and enumerates sessions/logged on users as well as local administrators.
Ships all data off to a BloodHound RESTapi instance with Export-BloodHoundData.

View File

@ -1,811 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Bloodhound</title>
<link rel="icon" href="img/favicon.ico">
<!--<script src="js/jquery-2.2.1.min.js" onload="$ = jQuery = module.exports;"></script>-->
<script>window.$ = window.jQuery = require('jquery');</script>
<script type="text/javascript" src="js/jquery-ui.min.js"></script>
<script type="text/javascript" src="js/circle-progress/circle-progress.js"></script>
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<script type="text/javascript" src="js/bootstrap3-typeahead.min.js"></script>
<script type="text/javascript" src="js/sigma.min.js"></script>
<script type="text/javascript" src="js/plugins.min.js"></script>
<script type="text/javascript" src="js/mustache.min.js"></script>
<script type="text/javascript" src="js/bloodhound.js"></script>
<script type="text/javascript" src="js/sigma.renderers.linkurious.min.js"></script>
<script type="text/javascript" src="js/sigma.plugins.design.min.js"></script>
<script type="text/javascript" src="js/sigma.layouts.dagre.min.js"></script>
<script type="text/javascript" src="js/sigma.layouts.noverlap.js"></script>
<script type="text/javascript" src="js/sigma.renderers.glyphs.min.js"></script>
<script type="text/javascript" src="js/sigma.exporters.image.min.js"></script>
<script type="text/javascript" src="js/dagre.min.js"></script>
<script type="text/javascript" src="js/simple-slider.min.js"></script>
<link rel="stylesheet" type="text/css" href="css/font-awesome.min.css">
<link type="text/css" rel="stylesheet" href="css/bootstrap.min.css">
<link type="text/css" rel="stylesheet" href="css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/animation.css">
<link rel="stylesheet" type="text/css" href="css/tooltip.css">
<link rel="stylesheet" type="text/css" href="css/simple-slider.css">
<link rel="stylesheet" type="text/css" href="css/simple-slider-volume.css">
<script type="javascript/worker" id="ingestworker">
function createXHR(url, auth){
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", url, false)
xmlhttp.setRequestHeader('Accepts', 'application/json');
xmlhttp.setRequestHeader('Content-Type', 'application/json');
xmlhttp.setRequestHeader('Authorization', auth);
xmlhttp.setRequestHeader('X-Stream', true);
return xmlhttp
}
self.addEventListener('message', function(e){
var currentTransaction = null;
var groups = [], i;
var sent = 0
importScripts(e.data.url + "/js/papaparse.min.js");
var data = Papa.parse(e.data.data, {header:true, skipEmptyLines: true}).data;
var testElement = data[0];
for (i=0; i < data.length; i += 1000){
groups.push(data.slice(i, i+1000));
}
if (e.data.type === 'localadmin'){
var starttime = new Date();
if (testElement.hasOwnProperty('Server') && testElement.hasOwnProperty('AccountName') && testElement.hasOwnProperty('IsGroup')){
var topost = []
topost.message = 'start'
topost.len = data.length
postMessage(topost)
var xmlhttp = createXHR(e.data.dburl + '/db/data/transaction', e.data.auth);
xmlhttp.send()
currentTransaction = JSON.parse(xmlhttp.responseText).commit.split('/').slice(-2)[0]
var postdata = null
groups.forEach(function(g){
postdata = {}
postdata.statements = []
g.forEach(function(o){
if (o.IsGroup == 'False'){
var user = o['AccountName'].replace('\\','/').split('/')[1].toUpperCase()
var computer = o['Server'].toUpperCase()
var obj = {
"statement": 'MERGE (user:User {name: "' + user + '"}) WITH user MERGE (computer:Computer {name: "' + computer + '" }) WITH user,computer MERGE (user)-[:AdminTo]->(computer)'
}
postdata.statements.push(obj)
}else{
var group = o['AccountName'].replace('\\','/').split('/')[1].toUpperCase()
var computer = o['Server'].toUpperCase()
var obj = {
"statement": 'MERGE (group:Group {name: "' + group + '"}) WITH group MERGE (computer:Computer {name: "' + computer +'"}) WITH group,computer MERGE (group)-[:AdminTo]->(computer)'
}
postdata.statements.push(obj)
}
})
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction, e.data.auth)
xmlhttp.send(JSON.stringify(postdata));
sent = sent + g.length
var topost = []
topost.message = 'progress'
topost.len = data.length
topost.progress = sent
postMessage(topost)
if ((sent % 20000) == 0){
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction + "/commit", e.data.auth);
xmlhttp.send()
var topost = []
topost.message = 'commit'
postMessage(topost)
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction", e.data.auth);
xmlhttp.send()
currentTransaction = JSON.parse(xmlhttp.responseText).commit.split('/').slice(-2)[0]
}
})
var xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction + "/commit", e.data.auth);
xmlhttp.send()
var endtime = new Date()
var timediff = Math.abs(endtime.getTime() - starttime.getTime())
var diffsec = Math.ceil(timediff / 1000)
var topost = []
topost.message = 'end'
topost.len = data.length
topost.time = diffsec
postMessage(topost)
}else{
var topost = []
topost.message = 'baddata'
postMessage(topost)
}
}else if (e.data.type == 'domainmembership'){
var starttime = new Date();
if (testElement.hasOwnProperty('MemberName') && testElement.hasOwnProperty('GroupName') && testElement.hasOwnProperty('IsGroup')){
var topost = []
topost.message = 'start'
topost.len = data.length
postMessage(topost)
var xmlhttp = createXHR(e.data.dburl + '/db/data/transaction', e.data.auth);
xmlhttp.send()
currentTransaction = JSON.parse(xmlhttp.responseText).commit.split('/').slice(-2)[0]
var postdata = null
groups.forEach(function(g){
postdata = {}
postdata.statements = []
g.forEach(function(o){
if (o.IsGroup == 'False'){
var user = o['MemberName'].toUpperCase()
var group = o['GroupName'].toUpperCase()
var obj = {
"statement": 'MERGE (user:User {name: "' + user + '"}) WITH user MERGE (group:Group {name: "' + group + '" }) WITH user,group MERGE (user)-[:MemberOf]->(group)'
}
postdata.statements.push(obj)
}else{
var ga = o['MemberName'].toUpperCase()
var gb = o['GroupName'].toUpperCase()
var obj = {
"statement": 'MERGE (group1:Group {name: "' + ga + '"}) WITH group1 MERGE (group2:Group {name: "' + gb +'"}) WITH group1,group2 MERGE (group1)-[:MemberOf]->(group2)'
}
postdata.statements.push(obj)
}
})
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction, e.data.auth)
xmlhttp.send(JSON.stringify(postdata));
sent = sent + g.length
var topost = []
topost.message = 'progress'
topost.len = data.length
topost.progress = sent
postMessage(topost)
if ((sent % 20000) == 0){
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction + "/commit", e.data.auth);
xmlhttp.send()
var topost = []
topost.message = 'commit'
postMessage(topost)
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction", e.data.auth);
xmlhttp.send()
currentTransaction = JSON.parse(xmlhttp.responseText).commit.split('/').slice(-2)[0]
}
})
var xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction + "/commit", e.data.auth);
xmlhttp.send()
var endtime = new Date()
var timediff = Math.abs(endtime.getTime() - starttime.getTime())
var diffsec = Math.ceil(timediff / 1000)
var topost = []
topost.message = 'end'
topost.len = data.length
topost.time = diffsec
postMessage(topost)
}else{
var topost = []
topost.message = 'baddata'
postMessage(topost)
}
}else if (e.data.type == 'sessions'){
var starttime = new Date();
if (testElement.hasOwnProperty('Computer') && testElement.hasOwnProperty('User')){
var topost = []
topost.message = 'start'
topost.len = data.length
postMessage(topost)
var xmlhttp = createXHR(e.data.dburl + '/db/data/transaction', e.data.auth);
xmlhttp.send()
currentTransaction = JSON.parse(xmlhttp.responseText).commit.split('/').slice(-2)[0]
var postdata = null
groups.forEach(function(g){
postdata = {}
postdata.statements = []
g.forEach(function(o){
var user = o['User'].toUpperCase()
var computer = o['Computer'].toUpperCase()
var obj = {
"statement": 'MERGE (user:User {name: "' + user + '"}) WITH user MERGE (computer:Computer {name: "' + computer + '" }) WITH user,computer MERGE (computer)-[:HasSession]->(user)'
}
postdata.statements.push(obj)
})
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction, e.data.auth)
xmlhttp.send(JSON.stringify(postdata));
sent = sent + g.length
var topost = []
topost.message = 'progress'
topost.len = data.length
topost.progress = sent
postMessage(topost)
if ((sent % 20000) == 0){
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction + "/commit", e.data.auth);
xmlhttp.send()
var topost = []
topost.message = 'commit'
postMessage(topost)
xmlhttp = createXHR(e.data.dburl + "/db/data/transaction", e.data.auth);
xmlhttp.send()
currentTransaction = JSON.parse(xmlhttp.responseText).commit.split('/').slice(-2)[0]
}
})
var xmlhttp = createXHR(e.data.dburl + "/db/data/transaction/" + currentTransaction + "/commit", e.data.auth);
xmlhttp.send()
var endtime = new Date()
var timediff = Math.abs(endtime.getTime() - starttime.getTime())
var diffsec = Math.ceil(timediff / 1000)
var topost = []
topost.message = 'end'
topost.len = data.length
topost.time = diffsec
postMessage(topost)
}else{
var topost = []
topost.message = 'baddata'
postMessage(topost)
}
}
}, false)
</script>
<script id="datatemplate" type="text/template">
<ul class='nav nav-tabs nav-justified' id="abc" role="tablist">
<li role='presentation' class='active'><a href='#dbinfo' role='tab' data-toggle='tab' aria-controls='dbinfo'>Database Info</a></li>
<li role='presentation'><a href='#nodeinfo' role='tab' data-toggle='tab' aria-controls='nodeinfo'>Node Info</a></li>
<li role='presentation'><a href='#pbqueries' role='tab' data-toggle='tab' aria-controls='pbqueries'>Queries</a></li>
</ul>
<div class="tab-content content">
<div role="tabpanel" class="tab-pane active" id="dbinfo">
{{{dbinfo}}}
</div>
<div role="tabpanel" class="tab-pane" id="nodeinfo">
{{{nodeinfo}}}
</div>
<div role="tabpanel" class="tab-pane" id="pbqueries">
<h3 align="center">
Pre-Built Analytics Queries
</h3>
<div style="padding: 0 .5em .5em .5em">
<a href="#" onclick="shortestPathsToDA()">Find Shortest Paths to DA</a><br>
<a href="#" onclick="doQuery('start n=node(*) match n<-[r:HasSession]-() WHERE NOT n.name=&quot;ANONYMOUS LOGON&quot; AND NOT n.name=&quot;&quot; with n, count(r) as rel_count order by rel_count desc LIMIT 1 MATCH (m)-[r:HasSession]->n RETURN n,r,m')">Find User with Most Sessions</a><br>
<a href="#" onclick="doQuery('MATCH (n:Domain) MATCH p=(n)-[r]-() RETURN p')">Map Domain Trusts</a><br>
</div>
</div>
</div>
</script>
<script id="dbdata" type="text/template">
<div>
<h3 align="center">Database Info</h3>
<dl class="dl-horizontal dl-horizontal-fix">
<dt>DB Address</dt>
<dd>{{url}}</dd>
<dt>DB User</dt>
<dd>{{user}}</dd>
<dt>Users</dt>
<dd>{{num_users}}</dd>
<dt>Computers</dt>
<dd>{{num_computers}}</dd>
<dt>Groups</dt>
<dd>{{num_groups}}</dd>
<dt>Relationships</dt>
<dd>{{num_relationships}}</dd>
</dl>
<div class="text-center">
<div class="dbbuttons">
<button type="button" class="btn btn-success" onclick="getDBInfo()">Refresh DB Stats</button>
<button type="button" class="btn btn-warning" data-toggle="modal" data-target="#logoutModal">Log Out/Switch DB</button>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#clearDBWarn">Clear Database</button>
</div>
</div>
</div>
</script>
<script id="computertemplate" type="text/template">
<dl class='dl-horizontal'>
<dt>Node</dt>
<dd>{{label}}</dd>
<dt>OS</dt>
<dd> None </dd>
<dt>Allows Unconstrained Delegation Name</dt>
<dd> None </dd>
<br>
<dt>Explicit Admins</dt>
<dd> <a href="#" onclick="doQuery('MATCH (n)-[r:AdminTo]->(m:Computer {name:&quot;{{label}}&quot;}) RETURN n,r,m')">{{explicit_admins}}</a></dd>
<dt>Unrolled Admins</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User),(target:Computer {name:&quot;{{label}}&quot;}), p=allShortestPaths((n)-[:AdminTo|MemberOf*1..]->(target)) RETURN p', &quot;{{label}}&quot;)">{{unrolled_admin}}</a></dd>
<br>
<dt>First Degree Group Membership</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:Computer {name:&quot;{{label}}&quot;}),(target:Group), (n)-[r:MemberOf]->(target) RETURN n,r,target', &quot;{{label}}&quot;)">{{first_degree_group}}</a></dd>
<dt>Unrolled Group Membership</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:Computer {name:&quot;{{label}}&quot;}), (target:Group),p=allShortestPaths((n)-[:MemberOf*1..]->(target)) RETURN p', &quot;{{label}}&quot;)">{{unrolled_group_membership}}</a></dd>
<dt>Sessions</dt>
<dd><a href="#" onclick="doQuery('MATCH (m:Computer {name:&quot;{{label}}&quot;})-[r:HasSession]->(n:User) WITH n,r,m WHERE NOT n.name ENDS WITH &quot;$&quot; RETURN n,r,m')">{{sessions}}</a></dd>
</dl>
</script>
<script id="grouptemplate" type="text/template">
<dl class='dl-horizontal'>
<dt>Node</dt>
<dd>{{label}}</dd>
<br>
<dt>Direct Members</dt>
<dd> <a href="#" onclick="doQuery('MATCH (n)-[r:MemberOf]->(m:Group {name:&quot;{{label}}&quot;}) RETURN n,r,m')">{{explicit_members}}</a></dd>
<dt>Unrolled Members</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User), (m:Group {name:&quot;{{label}}&quot;}), p=allShortestPaths((n)-[:MemberOf*1..]->(m)) RETURN p', &quot;{{label}}&quot;)">{{unrolled_members}}</a></dd>
<br>
<dt>Direct Admin To</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:Group {name:&quot;{{label}}&quot;})-[r:AdminTo]->(m:Computer) RETURN n,r,m', &quot;{{label}}&quot;)">{{adminto}}</a></dd>
<dt>Derivative Admin To</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:Group {name:&quot;{{label}}&quot;}), (target:Computer), p=allShortestPaths((n)-[*]->(target)) RETURN p', &quot;{{label}}&quot;)">{{derivative_admin}}</a>
<dt>Unrolled Member Of</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:Group {name:&quot;{{label}}&quot;}), (target:Group), p=allShortestPaths((n)-[r:MemberOf*1..]->(target)) RETURN p', &quot;{{label}}&quot;)">{{unrolled_member_of}}</a></dd>
<dt>Sessions</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User), (m:Group {name: &quot;{{label}}&quot;}), p=allShortestPaths((n)-[r:MemberOf*1..]->(m)) WITH n,m,r MATCH (n)-[s:HasSession]-(o:Computer) RETURN m,n,r,o,s',&quot;&quot;, &quot;{{label}}&quot;)">{{sessions}}</a></dd>
</dl>
</script>
<script id="ingestProgressBar" type="text/template">
<div class="panel-heading">
Ingesting Data
</div>
<div class="panel-body">
<div class="progress" style="width:100%">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="{{progress}}" aria-valuemin="0" aria-valuemax="{{total}}" style="width:{{width}}%">
<span>{{progress}} / {{total}}</span>
</div>
</div>
</div>
<div class="panel-footer">
<button id="startingestbutton" class="btn btn-large" onclick="cancelIngest()">Cancel</button>
</div>
</script>
<script id="ingestComplete" type="text/template">
<div class="panel-heading">
Ingest Finished
</div>
<div class="panel-body">
<div class="progress" style="width:100%">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="{{total}}" aria-valuemin="0" aria-valuemax="{{total}}" style="width:100%">
</div>
</div>
<div class="text-center">
<span>Ingest Complete in {{time}} seconds! <i class="fa fa-check-circle green-icon-color"></i></span>
</div>
</div>
</script>
<script id="ingestCancelled" type="text/template">
<div class="panel-heading">
Ingest Cancelled
</div>
<div class="panel-body">
<div class="text-center">
<span>Ingest Cancelled! <i class="fa fa-times-circle red-icon-color"></i></span>
</div>
</div>
</script>
</head>
<body role="document">
<div id="overlay" class="loginwindow">
<div id="loginpanel">
<img src="img/logo-white-transparent-full.png">
<div class="text-center">
<span>Log in to Neo4j Database</span>
</div>
<form>
<div class="form-group has-feedback">
<div class="input-group">
<span class="input-group-addon" id="dburladdon">Database URL</span>
<input id="dburl" type="text" class="form-control" placeholder="http://db-ip:dp-port" aria-describedby="dburladdon">
<i id="dburlspinner" class="fa fa-spinner fa-spin form-control-feedback"></i>
</div>
<p id="dbHelpBlock" class="help-block help-block-add hide">No Neo4j REST API Found</p>
<div class="input-group spacing">
<span class="input-group-addon" id="dbuseraddon">&nbsp;DB Username</span>
<input id="dbusername" type="text" class="form-control" placeholder="neo4j" aria-describedby="dbuseraddon">
</div>
<div class="input-group spacing">
<span class="input-group-addon" id="dbpwaddon">&nbsp;DB Password</span>
<input id="dbpassword" type="password" class="form-control" placeholder="neo4j" aria-describedby="dbpwaddon">
</div>
<p id="loginbadpw" class="help-block help-block-add hide" style="color: #d9534f">Wrong username or password</p>
<button id="loginbutton" disabled=true type="button" class="btn btn-primary loginbutton has-spinner">
Login
<span class="spinner"><i class="fa fa-spinner fa-spin"></i></span>
</button>
</div>
</form>
</div>
<div id="checkconnectionpanel">
<span id="connectionstatus">Checking Stored Credentials <i class="fa fa-spinner fa-spin"></i></span>
</div>
<div id="settingspanel">
</div>
</div>
<div id="nodataalert" class="alert alert-warning alert-dismissible alertdiv" role="alert">
<button type="button" class="close" onclick="$('#nodataalert').fadeToggle(false)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>No data returned from query.
</div>
<div id="layoutchange" class="alert alert-warning alert-dismissible alertdiv" role="alert">
<button type="button" class="close" onclick="$('#layoutchange').fadeToggle(false)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>Changed Layout To Hierarchical
</div>
<div id="graph" class="graph"> </div>
<div id= "bottomdiv" class="bottomdiv">
<button id="bottomSlide" class="slideupbutton">
<span class="glyphicon glyphicon-chevron-up"></span> Raw Query <span class="glyphicon glyphicon-chevron-up"></span>
</button>
<input type="text" class="form-control queryInput" autocomplete="off" id="rawQueryBox" placeholder="Enter a raw query...">
</div>
<div id="searchDiv" class="searchdiv">
<div class="input-group input-group-unstyled">
<span id="menu" class="input-group-addon spanfix" data-toggle="tooltip" data-placement="bottom" title="More Info">
<i class="glyphicon glyphicon-menu-hamburger menuglyph"></i></span>
<input id="searchBar" type="search" class="form-control searchbox" autocomplete="off" placeholder="Start typing to search for a node...">
<span id="openpath" class="input-group-addon spanfix" data-toggle="tooltip" data-placement="bottom" title="Pathfinding">
<i class="glyphicon glyphicon glyphicon-road menuglyph"></i></span>
<span id="back" class="input-group-addon spanfix" data-toggle="tooltip" data-placement="bottom" title="Back">
<i class="glyphicon glyphicon-step-backward menuglyph"></i></span>
</div>
<div id="pathfindingbox">
<div class="input-group input-group-unstyled">
<span class="input-group-addon spanfix" style="visibility: hidden">
<i class="glyphicon glyphicon-menu-hamburger menuglyph"></i></span>
<input id="endNode" type="search" class="form-control searchbox" autocomplete="off" placeholder="Target Node">
<span class="input-group-addon spanfix" style="visibility: hidden">
<i class="glyphicon glyphicon glyphicon-road menuglyph"></i></span>
<span id="play" class="input-group-addon spanfix" data-toggle="tooltip" data-placement="bottom" title="Findpath">
<i class="glyphicon glyphicon-play menuglyph"></i></span>
</div>
</div>
<div class="nodedatabox" id="nodedatabox">
</div>
</div>
<div id="menuDiv" class="menudiv">
<div>
<button id="refreshbutton" class="btn">
<span class="glyphicon glyphicon-refresh rightspan"></span>
</button>
</div>
<div>
<button id="exportbutton" class="btn">
<span class="glyphicon glyphicon-export rightspan"></span>
</button>
</div>
<div>
<button id="importbutton" class="btn">
<span class="glyphicon glyphicon-import rightspan"></span>
</button>
</div>
<div>
<button id="uploadbutton" class="btn">
<span class="glyphicon glyphicon-upload rightspan"></span>
</button>
</div>
<div>
<button id="layoutbutton" class="btn">
<span style="width:14px" class="fa fa-line-chart rightspan"></span>
</button>
</div>
<div>
<button id="settingsbutton" class="btn">
<span style="width:14px" class="fa fa-cogs rightspan"></span>
</button>
</div>
</div>
<div id="uploadSelectDiv" class="uploadSelectDiv panel panel-default">
<div class="panel-heading">
Ingest CSV
<button type="button" class="close" onclick="$('#uploadSelectDiv').fadeToggle(false)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="panel-body ingestinput">
<div class="input-group" id="fileselectdiv">
<span class="input-group-addon" id="addonforfile">File Name:</span>
<input id="uploadFileSelected" aria-described-by="addonforfile" class="form-control">
<span class="input-group-btn">
<button id="selectUploadFile" class="btn btn-default">
<span class="glyphicon glyphicon-file"></span>
</button>
</span>
</div>
<div class="list-group" style="margin-top:15px;">
<a href="#" id="ingestlocaladmin" class="list-group-item active">
<h4 class="list-group-item-heading">Local Admin Membership</h4>
<p class="list-group-item-text">
Data containing members of local Administrator groups
</p>
</a>
<a href="#" id="ingestdomaingroup" class="list-group-item">
<h4 class="list-group-item-heading">Domain Group Membership</h4>
<p class="list-group-item-text">
Data containing members of domain groups
</p>
</a>
<a href="#" id="ingestusersessions" class="list-group-item">
<h4 class="list-group-item-heading">User Sessions</h4>
<p class="list-group-item-text">
Data containing login sessions for users
</p>
</a>
</div>
<div style="text-align: center">
<button id="startingestbutton" class="btn btn-large">Ingest Data</button>
</div>
</div>
</div>
<div id="exportSelectDiv" class="uploadSelectDiv panel panel-default">
<div class="panel-heading">
Export Graph
<button type="button" class="close" onclick="$('#exportSelectDiv').fadeToggle(false)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="panel-body">
<div class="list-group">
<a href="#" id="exportjson" class="list-group-item active">
<h4 class="list-group-item-heading">Export to JSON</h4>
<p class="list-group-item-text">
Use this format to export data and re-import it later
</p>
</a>
<a href="#" id="exportimage" class="list-group-item">
<h4 class="list-group-item-heading">Export to Image</h4>
<p class="list-group-item-text">
Use this format to export data and view it as an image
</p>
</a>
</div>
<div style="text-align: center">
<button id="exportFinishButton" class="btn btn-lg">Export Data</button>
</div>
</div>
</div>
<button id="refreshbuttonhidden" class="btn rightmenubutton hideme">
Refresh<span class="glyphicon glyphicon-refresh rightglyph"></span>
</button>
<button id="exportbuttonhidden" class="btn rightmenubutton hideme">
Export Graph<span class="glyphicon glyphicon-export rightglyph"></span>
</button>
<button id="importbuttonhidden" class="btn rightmenubutton hideme">
Import Graph<span class="glyphicon glyphicon-import rightglyph"></span>
</button>
<button id="uploadbuttonhidden" class="btn rightmenubutton hideme">
Upload Data<span class="glyphicon glyphicon-upload rightglyph"></span>
</button>
<button id="layoutbuttonhidden" class="btn rightmenubutton hideme">
Change Layout Type<span class="fa fa-line-chart rightglyph"></span>
</button>
<button id="settingsbuttonhidden" class="btn rightmenubutton hideme">
Settings<span class="fa fa-cogs rightglyph"></span>
</button>
<input type="file" accept=".json" id="fileloader" name="files" title="Open JSON" style="display:none">
<input type="file" accept=".csv" id="uploader" name="import" title="Open CSV for Import" style="display:none">
<div id="logoutModal" class="modal fade" role="dialog" aria-labelledby="logoutModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="logoutModalLabel">Logout</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to logout?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" onclick="resetUI()">Logout</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<div id="clearDBWarn" class="modal fade" role="dialog" aria-labelledby="clearDBWarnLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="clearDBWarnLabel">Clear Database</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to clear the database? This is irreversible and may take some time!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#clearDBConfirm" data-dismiss="modal">Clear Database</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<div id="clearDBConfirm" class="modal fade" role="dialog" aria-labelledby="clearDBConfirmLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="clearDBConfirmLabel">Clear Database</h4>
</div>
<div class="modal-body">
<p>Are you ABSOLUTELY sure you want to clear the database?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" data-dismiss="modal" data-toggle="modal" data-target="#deleteProgressModal" onclick="clearDB()">Clear Database</button>
</div>
</div>
</div>
</div>
<div id="badDataModal" class="modal fade" role="dialog" aria-labelledby="badDataLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="badDataLabel">Invalid Data</h4>
</div>
<div class="modal-body">
<p>Provided CSV doesn't match the selected data type!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Ok</button>
</div>
</div>
</div>
</div>
<div id="deleteProgressModal" class="modal fade" role="dialog" data-keyboard="false" data-backdrop="static" aria-labelledby="deleteDBLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="deleteDBLabel">Clearing Database</h4>
</div>
<div class="modal-body">
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="zoomBox" class="zoomBox">
<div>
<button id="zoomIn" class="btn zoomIn">
<span class="fa fa-plus"></span>
</button>
</div>
<div>
<button id="zoomOut" class="btn zoomOut">
<span class="fa fa-minus"></span>
</button>
</div>
</div>
<div id="spotlight" class="spotlight">
<div class="input-group input-group-unstyled unborder">
<span class="input-group-addon spanfix">
<i class="glyphicon glyphicon-search"></i>
</span>
<input id="spotlightBar" type="search" class="form-control searchbox" autocomplete="off" placeholder="Explore Nodes" data-type="search">
</div>
<div class="nodeList">
<table id="spotlightTable" data-role="table" class="table table-striped" data-filter="true" data-input="#spotlightBar">
<thead style="text-align:center">
<tr>
<td>Node Label</td>
<td>Collapsed Into</td>
</tr>
</thead>
<tbody id="nodeBody" class="searchable">
</tbody>
</table>
</div>
</div>
<div id="nodeSelector" class="spotlight">
<div class="input-group input-group-unstyled unborder">
<span class="input-group-addon spanfix">
<i class="glyphicon glyphicon-search"></i>
</span>
<input id="nodeSelectorBar" type="search" class="form-control searchbox" autocomplete="off" placeholder="Filter Nodes" data-type="search">
</div>
<div class="nodeList">
<table id="nodeSelectorTable" data-role="table" class="table table-striped" data-filter="true" data-input="#nodeSelectorBar">
<thead style="text-align:center">
<tr>
<td>Choose a Node</td>
</tr>
</thead>
<tbody id="nodeSelectorBody" class="searchablex">
</tbody>
</table>
</div>
</div>
<div id="queryLoad" class="loadingIndicator">
<div id="loadingText">Loading</div>
<div id="circle"></div>
</div>
<div id="settingsDiv" class="settingsDiv panel panel-default">
<div class="panel-heading">
Settings
<button type="button" class="close" onclick="$('#settingsDiv').fadeToggle(false)" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="panel-body sliderfix">
<div>
<strong>Sibling Collapse Threshold</strong> <i data-toggle="tooltip" data-placement="right" title="Merge nodes that have the same parent. 0 to Disable, Default 10" id="siblingCollapseQuestion" class="glyphicon glyphicon-question-sign"></i><br>
<input type="text" data-slider="true" data-slider-range="0,20" data-slider-step="1" data-slider-theme="volume slideinline" id="siblingCollapseSlider">
<span>
<input type="number" min="0" max="20" class="sliderinput" id="siblingCollapseInput">
</span>
</div>
<div>
<strong>Node Collapse Threshold</strong> <i data-toggle="tooltip" data-placement="right" title="Collapse nodes at the end of paths that only have one relationship. 0 to Disable, Default 5" id="nodeCollapseQuestion" class="glyphicon glyphicon-question-sign"></i><br>
<input type="text" data-slider="true" data-slider-range="0,20" data-slider-step="1" data-slider-theme="volume slideinline" id="nodeCollapseSlider">
<span>
<input type="number" min="0" max="20" class="sliderinput" id="nodeCollapseInput">
</span>
</div>
<div class="checkbox-inline" style="padding-top:5px">
<label>
<input id="lowDetailCheck" type="checkbox"> Low Detail Mode
</label>
</div>
<i data-toggle="tooltip" data-placement="right" title="Lower detail of graph to improve performance" id="siblingCollapseQuestion" class="glyphicon glyphicon-question-sign"></i>
</div>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

View File

@ -1,28 +0,0 @@
<script id="template" type="text/template">
<dl class='dl-horizontal'>
<dt>Node:</dt>
<dd>{{label}}</dd>
<dt>SAMAccountName</dt>
<dd> None </dd>
<dt>Display Name</dt>
<dd> None </dd>
<dt>Password Last Changed</dt>
<dd> None </dd>
<br>
<dt>First Degree Group Memberships</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Group),p=allShortestPaths((n)-[:MemberOf*1]->(target)) RETURN p')">{{first_degree_group}}</a></dd>
<dt>Unrolled Group Memberships</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Group),p=allShortestPaths((n)-[:MemberOf*1..]->(target)) RETURN p', &quot;{{label}}&quot;)">{{unrolled}}</a></dd>
<dt>Foreign Group Memberships</dt>
<dd></dd>
<br>
<dt>First Degree Local Admin</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Computer), p=allShortestPaths((n)-[:AdminTo*1]->(target)) RETURN p')">{{first_degree_admin}}</a></dd>
<dt>Group Delegated Local Admin Rights</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (m:Group), x=allShortestPaths((n)-[r:MemberOf*1..]->(m)) WITH n,m,r MATCH (m)-[s:AdminTo*1..]->(p:Computer) RETURN n,m,r,s,p', &quot;{{label}}&quot;, &quot;&quot;)">{{dlar}}</a></dd>
<dt>Derivative Local Admin Rights</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (m:Computer), p=allShortestPaths((n)-[r*]->(m)) RETURN p', &quot;{{label}}&quot;)">{{derivative}}</a></dd>
<dt>Sessions</dt>
<dd><a href="#" onclick="doQuery('MATCH (n:Computer)-[r:HasSession]->(m:User {name:&quot;{{label}}&quot;}) RETURN n,r,m')">{{sessions}}</a></dd>
</dl>
</script>

34
index.html Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<link type="text/css" rel="stylesheet" href="src/css/font-awesome.min.css">
<link type="text/css" rel="stylesheet" href="src/css/bootstrap.min.css">
<link type="text/css" rel="stylesheet" href="src/css/styles.css">
<script>window.$ = window.jQuery = require('jquery');</script>
<script>window.sigma = require('linkurious');</script>
<script src="src/js/plugins.min.js" type="text/javascript"></script>
<script src="src/js/bootstrap3-typeahead.min.js" type="text/javascript"></script>
</head>
<body>
<div id="root">
</div>
<script>
(function() {
const script = document.createElement('script');
if (process.env.ENV === 'development '){
script.src = 'http://localhost:9000/dist/bundle.js';
}else{
script.src = './dist/bundle.js';
}
document.write(script.outerHTML);
}());
</script>
</body>
<script>
// You can also require other files to run in this process
require('./renderer.js')
</script>
</html>

42
main.js
View File

@ -1,53 +1,53 @@
const electron = require('electron');
const electron = require('electron')
// Module to control application life.
const {app} = electron;
const app = electron.app
// Module to create native browser window.
const {BrowserWindow} = electron;
const BrowserWindow = electron.BrowserWindow
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;
let mainWindow
function createWindow() {
function createWindow () {
// Create the browser window.
win = new BrowserWindow({width: 1920, height: 1080});
mainWindow = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
win.loadURL(`file://${__dirname}/app/Bloodhound.html`);
mainWindow.loadURL(`file://${__dirname}/index.html`)
// Open the DevTools.
win.webContents.openDevTools();
mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
win.on('closed', () => {
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
});
mainWindow = null
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
app.on('ready', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
app.quit()
}
});
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
if (mainWindow === null) {
createWindow()
}
});
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

View File

@ -1,22 +1,47 @@
{
"name": "bloodhound",
"version": "0.1.0",
"main": "./main.js",
"productName": "Bloodhound",
"devDependencies": {
"electron-packager": "^7.3.0",
"electron-prebuilt": "^1.2.7"
},
"build": {
"appId": "12345",
"app-category-type": "your.app.category.type",
"win": {
}
},
"version": "1.0.0",
"description": "Graph Theory for Active Directory",
"main": "main.js",
"scripts": {
"postinstall": "install-app-deps",
"pack": "build --dir",
"dist": "build",
"start": "electron ."
"start": "set ENV=development & electron .",
"server": "babel-node server.js"
},
"babel": {
"presets": [
"es2015",
"stage-0",
"react"
]
},
"author": "@CptJesus",
"license": "CC0-1.0",
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-core": "^6.11.4",
"babel-loader": "^6.2.4",
"babel-polyfill": "^6.9.1",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-0": "^6.5.0",
"electron-prebuilt": "^1.2.8",
"express": "^4.14.0",
"webpack": "^1.13.1",
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.12.1",
"webpack-target-electron-renderer": "^0.4.0"
},
"dependencies": {
"bootstrap": "^3.3.6",
"bootstrap-3-typeahead": "^4.0.1",
"configstore": "^2.0.0",
"electron-json-storage": "^2.0.0",
"eventemitter2": "^2.0.0",
"jquery": "^2.2.4",
"linkurious": "^1.5.1",
"react": "^15.2.1",
"react-bootstrap": "^0.30.0",
"react-dom": "^15.2.1",
"react-if": "^2.1.0"
}
}

3
renderer.js Normal file
View File

@ -0,0 +1,3 @@
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// All of the Node.js APIs are available in this process.

20
server.js Normal file
View File

@ -0,0 +1,20 @@
import express from 'express';
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import config from './webpack.config.development';
const compiler = webpack(config);
const app = express();
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
stats: {
colors: true
}
}));
app.use(webpackHotMiddleware(compiler));
app.listen(9000);

View File

@ -0,0 +1,22 @@
import React, { Component } from 'react';
import GraphContainer from './graph'
import SearchContainer from './SearchContainer/searchcontainer'
import SpotlightContainer from './Spotlight/spotlightcontainer'
import LogoutModal from './Modals/logoutmodal'
export default class AppContainer extends Component {
constructor(){
super();
}
render() {
return (
<div className="max">
<GraphContainer />
<SearchContainer />
<SpotlightContainer/>
<LogoutModal/>
</div>
);
};
}

View File

@ -0,0 +1,33 @@
import React, { Component } from 'react';
import { If, Then, Else } from 'react-if';
export default class GlyphiconSpan extends Component {
propTypes: {
classes : React.PropTypes.string,
tooltipDir : React.PropTypes.string,
tooltipTitle : React.PropTypes.string,
tooltip : React.PropTypes.bool.isRequired,
click: React.PropTypes.func
}
constructor(props){
super(props);
}
render() {
return (
<If condition={ this.props.tooltip }>
<Then>
<span onClick={this.props.click} className={this.props.classes} data-toggle="tooltip" data-placement={this.props.tooltipDir} title={this.props.tooltipTitle}>
{this.props.children}
</span>
</Then>
<Else>{() =>
<span onClick={this.props.click} className={this.props.classes}>
{this.props.children}
</span>
}</Else>
</If>
);
}
}

158
src/components/Graph.jsx Normal file
View File

@ -0,0 +1,158 @@
import React, { Component } from 'react';
import { collapseEdgeNodes, setNodeData, collapseSiblingNodes } from '../js/utils.js';
export default class GraphContainer extends Component {
constructor(props){
super(props)
this.state = {
sigmaInstance : null,
design: null,
dragged: false
}
}
render() {
return (
<div id="graph" className="graph"></div>
);
}
doQueryNative(params){
sigma.neo4j.cypher({
url: appStore.databaseInfo.url,
user: appStore.databaseInfo.user,
password: appStore.databaseInfo.password
},
params.statement,
this.state.sigmaInstance,
function(sigmaInstance){
var design = this.state.design;
sigmaInstance = setNodeData(this.state.sigmaInstance);
if (params.allowCollapse){
sigmaInstance = collapseEdgeNodes(sigmaInstance);
sigmaInstance = collapseSiblingNodes(sigmaInstance);
}
this.state.sigmaInstance = sigmaInstance
design.deprecate();
design.apply();
sigmaInstance.refresh();
this.state.design = design;
sigma.misc.animation.camera(sigmaInstance.camera, { x: 0, y: 0, ratio: 1.075 });
sigma.layouts.startForceLink()
}.bind(this))
}
doQueryEvent(){
this.doQueryNative({
statement: 'MATCH (n:Group) WHERE n.name =~ "(?i).*DOMAIN ADMINS.*" WITH n MATCH (n)<-[r:MemberOf]-(m) RETURN n,r,m',
allowCollapse: false
})
}
_nodeDragged(){
this.setState({dragged:true})
}
_nodeClicked(n){
if (!this.state.dragged){
if (n.data.node.type_user){
emitter.emit('userNodeClicked', n.data.node.label)
}
}else{
this.setState({dragged: false})
}
}
componentWillMount() {
emitter.on('query', this.doQueryEvent.bind(this))
}
componentDidMount() {
//Sigma Initialization
var sigmaInstance, design;
sigma.renderers.def = sigma.renderers.canvas;
sigma.classes.graph.addMethod('outboundNodes', function(id) {
return this.outNeighborsIndex.get(id).keyList();
});
sigma.classes.graph.addMethod('inboundNodes', function(id) {
return this.inNeighborsIndex.get(id).keyList();
});
sigmaInstance = new sigma(
{
container: 'graph'
}
)
sigmaInstance.settings(
{
edgeColor: 'default',
nodeColor: 'default',
minEdgeSize: 1,
maxEdgeSize: 2.5,
iconThreshold: 4,
labelThreshold: 15,
labelAlignment: 'bottom',
labelColor: 'default',
font: 'Roboto',
glyphFillColor: 'black',
glyphTextColor: 'white',
glyphTextThreshold: 1,
zoomingRatio: 1.4
}
)
sigmaInstance.renderers[0].bind('render', function(e) {
sigmaInstance.renderers[0].glyphs();
});
sigmaInstance.camera.bind('coordinatesUpdated', function(e){
if (e.target.ratio > 1.25){
sigmaInstance.settings('drawEdgeLabels', false);
}else{
sigmaInstance.settings('drawEdgeLabels', true);
}
})
var dragListener = sigma.plugins.dragNodes(sigmaInstance,
sigmaInstance.renderers[0])
dragListener.bind('drag', this._nodeDragged.bind(this))
sigmaInstance.bind('clickNode', this._nodeClicked.bind(this))
var fa = sigma.layouts.configForceLink(sigmaInstance, {
worker: true,
background: true,
easing: 'cubicInOut',
autoStop: true,
alignNodeSiblings: true,
barnesHutOptimize: true
});
var lowgfx = appStore.performance.lowGraphics
design = sigma.plugins.design(sigmaInstance);
if (lowgfx){
sigmaInstance.settings('defaultEdgeType', 'line');
sigmaInstance.settings('defaultEdgeColor', 'black');
design.setPalette(appStore.lowResPalette);
design.setStyles(appStore.lowResStyle);
}else{
sigmaInstance.settings('defaultEdgeType', 'tapered');
sigmaInstance.settings('defaultEdgeColor', '#356');
design.setPalette(appStore.highResPalette);
design.setStyles(appStore.highResStyle);
}
this.state.sigmaInstance = sigmaInstance;
this.state.design = design;
this.doQueryNative({
statement: 'MATCH (n:Group) WHERE n.name =~ "(?i).*DOMAIN ADMINS.*" WITH n MATCH (n)<-[r:MemberOf]-(m) RETURN n,r,m',
allowCollapse: false
})
}
}

18
src/components/Icon.jsx Normal file
View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
export default class Icon extends Component {
propTypes: {
glyph : React.PropTypes.string.isRequired,
extraClass : React.PropTypes.string
}
constructor(props){
super(props);
}
render() {
return (
<i className={"glyphicon glyphicon-" + this.props.glyph + " " + this.props.extraClass}></i>
);
}
}

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
var Modal = require('react-bootstrap').Modal;
export default class LogoutModal extends Component {
constructor(){
super();
this.state = {
open: false
}
}
closeModal(){
this.setState({ open: false })
}
closeAndLogout(){
this.setState({ open: false })
}
openModal(){
this.setState({open: true})
}
componentDidMount() {
}
render() {
return (
<Modal
show={this.state.open}
onHide={this.closeModal}
aria-labelledby="LogoutModalHeader"
ref="logoutModal">
<Modal.Header closeButton={true}>
<Modal.Title id="LogoutModalHeader">Logout</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Are you sure you want to logout?</p>
</Modal.Body>
<Modal.Footer>
<button type="button" className="btn btn-danger" onClick={this.closeAndLogout.bind(this)}>
Logout
</button>
<button type="button" className="btn btn-primary" onClick={this.closeModal.bind(this)}>
Cancel
</button>
</Modal.Footer>
</Modal>
);
}
}

View File

@ -0,0 +1,104 @@
import React, { Component } from 'react';
import { defaultAjaxSettings } from '../../js/utils.js';
import LogoutModal from '../Modals/logoutmodal';
export default class DatabaseDataDisplay extends Component {
constructor(){
super()
this.state = {
url: appStore.databaseInfo.url,
user: appStore.databaseInfo.user,
num_users: 'Refreshing',
num_computers: 'Refreshing',
num_groups: 'Refreshing',
num_relationships: 'Refreshing'
}
}
componentDidMount() {
this.refreshDBData()
}
toggleLogoutModal(){
}
render() {
return (
<div>
<h3>Database Info</h3>
<dl className="dl-horizontal dl-horizontal-fix">
<dt>DB Address</dt>
<dd>{this.state.url}</dd>
<dt>DB User</dt>
<dd>{this.state.user}</dd>
<dt>Users</dt>
<dd>{this.state.num_users}</dd>
<dt>Computers</dt>
<dd>{this.state.num_computers}</dd>
<dt>Groups</dt>
<dd>{this.state.num_groups}</dd>
<dt>Relationships</dt>
<dd>{this.state.num_relationships}</dd>
</dl>
<div className="text-center">
<div className="btn-group dbbuttons">
<button type="button" className="btn btn-success" onClick={function(){emitter.emit('query')}}>Refresh DB Stats</button>
<button type="button" className="btn btn-warning" onClick={this.toggleLogoutModal}>Log Out/Switch DB</button>
<button type="button" className="btn btn-danger" data-toggle="modal" data-target="#clearDBWarn">Clear Database</button>
</div>
</div>
</div>
);
}
refreshDBData(){
var user,computer,group,relationship;
user = defaultAjaxSettings();
computer = defaultAjaxSettings();
group = defaultAjaxSettings();
relationship = defaultAjaxSettings();
user.data = JSON.stringify({
'statements': [{
'statement': "MATCH (n:User) WHERE NOT n.name ENDS WITH '$' RETURN count(n)"
}]
})
user.success = function(json){
this.setState({num_users:json.results[0].data[0].row[0] })
}.bind(this)
group.data = JSON.stringify({
'statements': [{
'statement': "MATCH (n:Group) RETURN count(n)"
}]
})
group.success = function(json){
this.setState({num_groups:json.results[0].data[0].row[0] })
}.bind(this)
computer.data = JSON.stringify({
'statements': [{
'statement': "MATCH (n:Computer) RETURN count(n)"
}]
})
computer.success = function(json){
this.setState({num_computers:json.results[0].data[0].row[0] })
}.bind(this)
relationship.data = JSON.stringify({
'statements': [{
'statement': "MATCH ()-[r]->() RETURN count(r)"
}]
})
relationship.success = function(json){
this.setState({num_relationships:json.results[0].data[0].row[0] })
}.bind(this)
$.ajax(user);
$.ajax(computer);
$.ajax(group);
$.ajax(relationship);
}
}

View File

@ -0,0 +1,20 @@
import React, { Component } from 'react';
export default class NoNodeData extends Component {
propTypes: {
visible : React.PropTypes.bool.isRequired
}
render() {
return (
<div className={this.props.visible ? "" : "hidden"}>
<h3>
Node Properties
</h3>
<p>
Select a node for more information
</p>
</div>
);
}
}

View File

@ -0,0 +1,23 @@
import React, { Component } from 'react';
import { If, Then, Else } from 'react-if';
export default class NodeALink extends Component {
propTypes: {
ready : React.PropTypes.bool.isRequired,
click : React.PropTypes.func,
value : React.PropTypes.number
}
constructor(props){
super(props);
}
render() {
return (
<If condition={this.props.ready}>
<Then><a href="#" onClick={this.props.click}>{this.props.value}</a></Then>
<Else>{() =>
<a style={{color:'black'}}>-</a>
}</Else>
</If>
);
}
}

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
export default class PrebuiltQueriesDisplay extends Component {
render() {
return (
<div>
<h3>
Pre-Built Analytics Queries
</h3>
<div className="query-box">
<a href="#" >Find Shortest Paths to DA</a><br />
<a href="#" >Find User with Most Sessions</a><br />
<a href="#" >Map Domain Trusts</a><br />
</div>
</div>
);
}
}

View File

@ -0,0 +1,179 @@
import React, { Component } from 'react';
import GlyphiconSpan from '../glyphiconspan'
import Icon from '../icon'
import { escapeRegExp } from '../../js/utils.js';
import TabContainer from './tabcontainer'
require('bootstrap-3-typeahead')
export default class SearchContainer extends Component {
constructor(props){
super(props)
this.state = {
data : [
{id: 1, name: 'John'},
{id: 2, name: 'Miles'},
{id: 3, name: 'Charles'},
{id: 4, name: 'Herbie'},
],
mainvalue : "",
pathvalue : "",
mainPlaceholder:"Start typing to search for a node...",
pathfindingIsOpen: false
}
}
_getSearchOptions(e, callback){
var x = []
var promise = $.ajax({
url: localStorage.getItem("dbpath") + "/db/data/transaction/commit",
type: 'POST',
accepts: { json: "application/json" },
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": localStorage.getItem("auth")
},
data: JSON.stringify({
"statements": [{
"statement" : "MATCH (n) WHERE n.name =~ '(?i).*" + escapeRegExp(e) + ".*' RETURN n.name LIMIT 10"
}]
}),
success: function(json) {
$.each(json.results[0].data, function(index, d){
x.push({id:index, label:d.row[0]})
})
callback(null, {
options: x,
complete: false
})
}.bind(this)
})
}
_onPathfindClick(){
jQuery(this.refs.pathfinding).slideToggle()
var p = !this.state.pathfindingIsOpen
var t = this.state.pathfindingIsOpen ? "Start typing to search for a node..." : "Start Node"
this.setState({
pathfindingIsOpen: p,
mainPlaceholder: t
})
}
_onPlayClick(){
}
_onExpandClick(){
jQuery(this.refs.tabs).slideToggle()
}
render(){
return (
<div className="searchdiv">
<div className="input-group input-group-unstyled">
<GlyphiconSpan
tooltip={true}
tooltipDir="bottom"
tooltipTitle="More Info"
classes="input-group-addon spanfix"
click={this._onExpandClick.bind(this)}>
<Icon glyph="menu-hamburger" extraClass="menuglyph" />
</GlyphiconSpan>
<input ref="searchbar" type="search" className="form-control searchbox" autoComplete="off" placeholder={this.state.mainPlaceholder} />
<GlyphiconSpan tooltip={true} tooltipDir="bottom"
tooltipTitle="Pathfinding"
classes="input-group-addon spanfix"
click={this._onPathfindClick.bind(this)}>
<Icon glyph="road" extraClass="menuglyph" />
</GlyphiconSpan>
<GlyphiconSpan
tooltip={true}
tooltipDir="bottom"
tooltipTitle="Back"
classes="input-group-addon spanfix">
<Icon glyph="step-backward" extraClass="menuglyph" />
</GlyphiconSpan>
</div>
<div ref="pathfinding">
<div className="input-group input-group-unstyled">
<GlyphiconSpan
tooltip={false}
classes="input-group-addon spanfix invisible">
<Icon glyph="menu-hamburger" extraClass="menuglyph" />
</GlyphiconSpan>
<input ref="pathbar" type="search" className="form-control searchbox" autoComplete="off" placeholder="Target Node" />
<GlyphiconSpan tooltip={false}
classes="input-group-addon spanfix invisible">
<Icon glyph="road" extraClass="menuglyph" />
</GlyphiconSpan>
<GlyphiconSpan
tooltip={true}
tooltipDir="bottom"
tooltipTitle="Find Path"
classes="input-group-addon spanfix"
click={this._onPlayClick.bind(this)}>
<Icon glyph="play" extraClass="menuglyph" />
</GlyphiconSpan>
</div>
</div>
<div ref="tabs">
<TabContainer />
</div>
</div>
)
}
openNodeTab(){
var e = jQuery(this.refs.tabs)
if (!(e.is(":visible"))){
e.slideToggle()
}
}
componentDidMount() {
jQuery(this.refs.pathfinding).slideToggle(0);
jQuery(this.refs.tabs).slideToggle(0);
emitter.on('userNodeClicked', this.openNodeTab.bind(this))
jQuery(this.refs.searchbar).typeahead({
source: function(query, process) {
return $.ajax({
url: localStorage.getItem("dbpath") + "/db/data/cypher",
type: 'POST',
accepts: { json: "application/json" },
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": "Basic bmVvNGo6bmVvNGpq"
},
data: JSON.stringify({ "query": "MATCH (n) WHERE n.name =~ '(?i).*" + escapeRegExp(query) + ".*' RETURN n.name LIMIT 10" }),
success: function(json) {
var d = json.data;
var l = d.length;
for (var i = 0; i < l; i++) {
d[i] = d[i].toString();
}
return process(json.data);
}
});
},
afterSelect: function(selected) {
if (!this.state.pathfindingIsOpen) {
doQuery("MATCH (n) WHERE n.name =~ '(?i).*" + escapeRegExp(selected) + ".*' RETURN n");
} else {
var start = $('#searchBar').val();
var end = $('#endNode').val();
if (start !== "" && end !== "") {
doQuery("MATCH (source {name:'" + start + "'}), (target {name:'" + end + "'}), p=allShortestPaths((source)-[*]->(target)) RETURN p", start, end);
}
}
}.bind(this),
autoSelect: false,
minLength: 3
})
}
}

View File

@ -0,0 +1,57 @@
import React, { Component } from 'react';
import DatabaseDataDisplay from './databasedatadisplay'
import PrebuiltQueriesDisplay from './prebuiltqueriesdisplay'
import NoNodeData from './nonodedata'
import UserNodeData from './usernodedata'
import { Tabs, Tab } from 'react-bootstrap';
export default class TabContainer extends Component {
constructor(props){
super(props);
this.state = {
userVisible: false,
computerVisible: false,
groupVisible: false,
selected: 1
}
}
_userNodeClicked(){
this.setState({
userVisible: true,
computerVisible: false,
groupVisible: false
})
this.setState({selected: 2})
}
componentDidMount() {
emitter.on('userNodeClicked', this._userNodeClicked.bind(this))
}
_handleSelect(index, last){
this.setState({selected: index})
}
render() {
return (
<div>
<Tabs id="tab-style" bsStyle="pills" activeKey={this.state.selected} onSelect={this._handleSelect.bind(this)}>
<Tab eventKey={1} title="Database Info">
<DatabaseDataDisplay />
</Tab>
<Tab eventKey={2} title="Node Info">
<NoNodeData visible={!this.state.userVisible && !this.state.computerVisible && !this.state.groupVisible}/>
<UserNodeData visible={this.state.userVisible}/>
</Tab>
<Tab eventKey={3} title="Queries">
<PrebuiltQueriesDisplay />
</Tab>
</Tabs>
</div>
);
}
}

View File

@ -0,0 +1,196 @@
import React, { Component } from 'react';
import NodeALink from './nodealink'
import { fullAjax } from '../../js/utils.js'
export default class UserNodeData extends Component {
propTypes: {
visible : React.PropTypes.bool.isRequired
}
constructor(){
super();
this.state = {
label: "",
samAccountName: "None",
displayName: "None",
pwdLastChanged: "None",
firstDegreeGroupMembership: -1,
unrolledGroupMembership: -1,
foreignGroupMembership: -1,
firstDegreeLocalAdmin: -1,
groupDelegatedLocalAdmin: -1,
derivateLocalAdmin: -1,
sessions: -1
}
emitter.on('userNodeClicked', this.getNodeData.bind(this));
}
placeholder(){
}
getNodeData(payload){
this.setState({
label: payload,
samAccountName: "None",
displayName: "None",
pwdLastChanged: "None",
firstDegreeGroupMembership: -1,
unrolledGroupMembership: -1,
foreignGroupMembership: -1,
firstDegreeLocalAdmin: -1,
groupDelegatedLocalAdmin: -1,
derivativeLocalAdmin: -1,
sessions: -1
})
var firstDegreeGroupMembership,
unrolledGroupMembership,
unrolledGroupMembership,
foreignGroupMembership,
firstDegreeLocalAdmin,
groupDelegatedLocalAdmin,
derivativeLocalAdmin,
sessions;
firstDegreeGroupMembership = fullAjax(
"MATCH (n:User {name:'{}'}), (m:Group), p=allShortestPaths((n)-[:MemberOf*1]->(m)) RETURN count(m)".format(payload),
function(json){
this.setState({firstDegreeGroupMembership: json.results[0].data[0].row[0]})
}.bind(this))
unrolledGroupMembership = fullAjax(
"MATCH (n:User {name:'{}'}), (target:Group), p=allShortestPaths((n)-[:MemberOf*1..]->(target)) RETURN count(target)".format(payload),
function(json){
this.setState({unrolledGroupMembership: json.results[0].data[0].row[0]})
}.bind(this))
firstDegreeLocalAdmin = fullAjax(
"MATCH (n:User {name:'{}}'}), (target:Computer), p=allShortestPaths((n)-[:AdminTo*1]->(target)) RETURN count(target)".format(payload),
function(json){
this.setState({firstDegreeLocalAdmin: json.results[0].data[0].row[0]})
}.bind(this)
)
groupDelegatedLocalAdmin = fullAjax(
"MATCH x=allShortestPaths((n:User {name:'{}'})-[r:MemberOf*1..]->(m:Group)) WITH n,m,r MATCH (m)-[s:AdminTo*1..]->(p:Computer) RETURN count(distinct(p))".format(payload),
function(json){
this.setState({groupDelegatedLocalAdmin: json.results[0].data[0].row[0]})
}.bind(this)
)
derivativeLocalAdmin = fullAjax(
"MATCH (n:User {name:'{}'}), (target:Computer), p=allShortestPaths((n)-[*]->(target)) RETURN count(distinct(target))".format(payload),
function(json){
this.setState({derivativeLocalAdmin: json.results[0].data[0].row[0]})
}.bind(this)
)
sessions = fullAjax(
"MATCH (n:Computer)-[r:HasSession]->(m:User {name:'{}'}) RETURN count(n)".format(payload),
function(json){
this.setState({sessions: json.results[0].data[0].row[0]})
}.bind(this)
)
$.ajax(firstDegreeGroupMembership);
$.ajax(unrolledGroupMembership);
$.ajax(firstDegreeLocalAdmin);
$.ajax(groupDelegatedLocalAdmin);
$.ajax(derivativeLocalAdmin);
$.ajax(sessions);
}
render() {
return (
<div className={this.props.visible ? "" : "displaynone"}>
<h3>
{this.state.label}
</h3>
<dl className='dl-horizontal'>
<dt>
SAMAccountName
</dt>
<dd>
{this.state.samAccountName}
</dd>
<dt>
Display Name
</dt>
<dd>
{this.state.displayName}
</dd>
<dt>
Password Last Changed
</dt>
<dd>
{this.state.pwdLastChanged}
</dd>
<br />
<dt>
First Degree Group Memberships
</dt>
<dd>
<NodeALink
ready={this.state.firstDegreeGroupMembership !== -1}
value={this.state.firstDegreeGroupMembership}
click={this.placeholder} />
</dd>
<dt>
Unrolled Group Memberships
</dt>
<dd>
<NodeALink
ready={this.state.unrolledGroupMembership !== -1}
value={this.state.unrolledGroupMembership}
click={this.placeholder} />
</dd>
<dt>
Foreign Group Membership
</dt>
<dd>
<NodeALink
ready={this.state.foreignGroupMembership !== -1}
value={this.state.foreignGroupMembership}
click={this.placeholder} />
</dd>
<br />
<dt>
First Degree Local Admin
</dt>
<dd>
<NodeALink
ready={this.state.firstDegreeLocalAdmin !== -1}
value={this.state.firstDegreeLocalAdmin}
click={this.placeholder} />
</dd>
<dt>
Group Delegated Local Admin Rights
</dt>
<dd>
<NodeALink
ready={this.state.groupDelegatedLocalAdmin !== -1}
value={this.state.groupDelegatedLocalAdmin}
click={this.placeholder} />
</dd>
<dt>
Derivate Local Admin Rights
</dt>
<dd>
<NodeALink
ready={this.state.derivativeLocalAdmin !== -1}
value={this.state.derivativeLocalAdmin}
click={this.placeholder} />
</dd>
<dt>
Sessions
</dt>
<dd>
<NodeALink
ready={this.state.sessions !== -1}
value={this.state.sessions}
click={this.placeholder} />
</dd>
</dl>
</div>
);
}
}

View File

@ -0,0 +1,43 @@
import React, { Component } from 'react';
import GlyphiconSpan from '../glyphiconspan';
import Icon from '../icon';
export default class SpotlightContainer extends Component {
constructor(props){
super(props);
this.state = {
data: {}
}
}
render() {
return (
<div ref="spotlight" className="spotlight">
<div className="input-group input-group-unstyled no-border-radius">
<GlyphiconSpan tooltip={false} classes="input-group-addon spanfix">
<Icon glyph="search" />
</GlyphiconSpan>
<input id="spotlight-search" type="search" className="form-control searchbox" autoComplete="off" placeholder="Explore Nodes" data-type="search" />
</div>
<div className="spotlight-nodelist">
<table data-role="table" className="table table-striped" data-filter="true" data-input="#spotlight-search">
<thead>
<tr>
<td>Node Label</td>
<td>Collapsed Into</td>
</tr>
</thead>
<tbody ref="spotlight-tbody" className="searchable">
</tbody>
</table>
</div>
</div>
);
}
componentDidMount() {
jQuery(this.refs.spotlight).fadeToggle(0)
}
}

View File

@ -0,0 +1,19 @@
import React, { Component } from 'react';
export default class SpotlightRow extends Component {
propTypes: {
node-id : React.PropTypes.number.isRequired,
parent-node-id : React.PropTypes.number.isRequired,
node-label : React.PropTypes.string.isRequired,
parent-node-label : React.PropTypes.string.isRequired
}
render() {
return (
<tr data-id={this.props.node-id} data-parent-id={this.props.parent-node-id}>
<td>{this.props.node-label}</td>
<td>{this.props.parent-node-label}</td>
</tr>
);
}
}

View File

@ -1,5 +1,16 @@
/* roboto-regular - latin */
#root {
position: absolute;
width: 100%;
height: 100%;
}
.max{
width: 100%;
height: 100%;
}
@font-face {
font-family: 'Roboto';
font-weight: 400;
@ -70,6 +81,10 @@ body {
outline: 0;
}
.query-box{
padding: 0 .5em .5em .5em
}
.graph {
width: 100%;
height: 97%;
@ -80,6 +95,39 @@ body {
outline: 0;
}
.typeaheadfix{
border: 0;
}
.typeaheadfix .Select-menu-outer{
z-index: 99999;
}
.typeaheadfix .Select-control{
border: 0;
box-shadow: none !important;
}
.typeaheadfix .Select-control:focus{
border: 0;
}
.typeaheadfix .Select-arrow-zone{
display: none;
}
@media all and (min-width: 900px) {
.searchdiv {
position: absolute;
z-index: 10;
top: 1em;
left: 1em;
overflow: visible;
width: 45%;
background-color: white;
}
}
@media all and (min-width: 1000px) {
.searchdiv {
position: absolute;
@ -173,10 +221,6 @@ body {
padding-bottom: 10px;
}
#abc li.active > a {
border-right: 1px solid lightgray;
border-left: 1px solid lightgray;
}
.hideme {
position: absolute;
@ -184,14 +228,6 @@ body {
visibility: hidden;
}
#abc li > a {
border-top: 0;
border-right: 0;
border-bottom: 1px solid lightgray;
border-left: 0;
border-radius: 0;
}
.content {
padding: 0 10px 0 10px;
}
@ -346,9 +382,7 @@ body {
opacity: 1;
}
.dbbuttons {
padding-bottom: 1em;
}
.modal {
text-align: center;
@ -407,7 +441,7 @@ body {
border-bottom-left-radius: 2em;
}
@media all and (min-width: 1000px) {
@media all and (min-width: 900px) {
.spotlight {
position: absolute;
z-index: 30;
@ -429,30 +463,24 @@ body {
}
}
.nodeList {
.spotlight-nodelist {
overflow: auto;
height: 11em;
background-color: white;
}
.nodelist table {
height: 100%;
border: 1px solid black;
.spotlight-nodelist table > thead {
text-align: center;
}
.unborder span {
.no-border-radius span {
border-radius: 0;
}
.unborder input {
.no-border-radius input {
border-radius: 0;
}
.nav-tabs li {
overflow: hidden;
white-space: nowrap;
}
.loadingIndicator {
position: absolute;
z-index: 25;
@ -493,3 +521,65 @@ body {
.sliderfix .slideinline > .dragger {
margin-top: -5px !important;
}
.center-align{
text-align: center;
}
#tab-style{
border-top: .5px solid lightgray;
}
#tab-style > ul{
display: table;
width: 100%;
border-bottom: 1px solid lightgray;
}
#tab-style > ul > li {
border-top: 0;
overflow: hidden;
white-space: nowrap;
border-radius: 0;
display: table-cell;
width: 33%;
text-align: center;
color: #337AB7;
}
#tab-style > ul > li > a:focus {
background-color: #fff;
color: black;
outline: none;
}
#tab-style > ul > li > a:hover {
background-color: #fff;
}
#tab-style > ul > li.active > a {
background-color: #fff;
color: black;
}
#tab-style > ul > li > a[aria-selected='true']{
border-right: 1px solid lightgray;
border-left: 1px solid lightgray;
color: black;
}
#tab-style > div{
padding: 0 10px 0 10px;
}
#tab-style h3{
text-align: center;
}
.dbbuttons {
padding-bottom: 1em;
}
.displaynone {
display: none;
}

View File

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 357 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

151
src/index.js Normal file
View File

@ -0,0 +1,151 @@
import 'babel-polyfill'; // generators
import React from 'react';
import ReactDOM from 'react-dom';
import AppContainer from './components/appcontainer';
import { getStorageData, storageHasKey, storageSetKey } from './js/utils.js';
const ConfigStore = require('configstore');
global.conf = new ConfigStore('bloodhound')
var e = require('eventemitter2').EventEmitter2
global.emitter = new e({})
String.prototype.format = function () {
var i = 0, args = arguments;
return this.replace(/{}/g, function () {
return typeof args[i] != 'undefined' ? args[i++] : '';
});
};
Array.prototype.allEdgesSameType = function() {
for (var i = 1; i < this.length; i++) {
if (this[i].neo4j_type !== this[0].neo4j_type)
return false;
}
return true;
};
global.PathfindingEvent = {
data: {
start: "",
end: ""
}
}
global.SearchEvent = {
data: {
label: ""
}
}
global.LogoutEvent = {
}
global.UserNodeClickEvent = {
data: {
label: ""
}
}
global.appStore = {
startNode: null,
endNode: null,
spotlightData: {},
highResPalette: {
iconScheme: {
'User': {
font: 'FontAwesome',
content: '\uF007',
scale: 1.5,
color: '#17E625'
},
'Computer': {
font: 'FontAwesome',
content: '\uF108',
scale: 1.2,
color: '#E67873'
},
'Group': {
font: 'FontAwesome',
content: '\uF0C0',
scale: 1.5,
color: '#DBE617'
},
'Domain': {
font: 'FontAwesome',
content: '\uF0AC',
scale: 1.5,
color: '#17E6B9'
}
}
},
lowResPalette: {
colorScheme: {
'User' : '#17E625',
'Computer' : '#E67873',
'Group' : '#DBE617',
'Domain' : '#17E6B9'
}
},
highResStyle: {
nodes: {
label: {
by: 'neo4j_data.name'
},
size: {
by: 'degree',
bins: 10,
min: 10,
max: 20
},
icon: {
by: 'neo4j_labels.0',
scheme: 'iconScheme'
}
}
},
lowResStyle: {
nodes: {
label: {
by: 'neo4j_data.name'
},
size: {
by: 'degree',
bins: 10,
min: 10,
max: 20
},
color: {
by: 'neo4j_labels.0',
scheme: 'colorScheme'
}
}
}
}
if (typeof conf.get('performance') === 'undefined'){
conf.set('performance', {
edge: 5,
sibling: 5,
lowGraphics: false
})
}
if (typeof conf.get('databaseInfo') === 'undefined'){
conf.set('databaseInfo', {
url: 'http://localhost:7474',
user: 'neo4j',
password: 'neo4jj'
})
}
appStore.performance = conf.get('performance')
appStore.databaseInfo = conf.get('databaseInfo');
ReactDOM.render(<AppContainer />, document.getElementById('root'))

25810
src/js/plugins.js Normal file

File diff suppressed because it is too large Load Diff

13
src/js/plugins.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
src/js/sigma.min.js vendored Normal file

File diff suppressed because one or more lines are too long

12227
src/js/sigma.require.js Normal file

File diff suppressed because it is too large Load Diff

249
src/js/utils.js Normal file
View File

@ -0,0 +1,249 @@
export function escapeRegExp(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\\\$&');
}
export function buildAuthHeader(){
db = storage.get('database-info', function(error, data){
return "Basic " + btoa(db.user + ":" + db.password)
})
}
//Graph Utils
export function setNodeData(sigmaInstance, startLabel, endLabel){
var startNode = null;
var endNode = null;
startLabel = (typeof startLabel === 'undefined') ? "" : startLabel
endLabel = (typeof endLabel === 'undefined') ? "" : endLabel
$.each(sigmaInstance.graph.nodes(), function(index, node){
node.degree = sigmaInstance.graph.degree(node.id);
});
$.each(sigmaInstance.graph.nodes(), function(index, node){
var s = node.neo4j_labels[0]
switch (s) {
case "Group":
node.type_group = true;
break;
case "User":
node.type_user = true;
break;
case "Computer":
node.type_computer = true;
break;
case "Domain":
node.type_domain = true;
break;
default:
console.log('Something has gone terribly wrong');
}
if (node.neo4j_data.name === startLabel){
appStore.startNode = node;
}
if (node.neo4j_data.name === endLabel){
appStore.endNode = node;
}
});
return sigmaInstance
}
export function collapseEdgeNodes(sigmaInstance){
var threshold = appStore.performance.edge;
if (threshold == 0){
return;
}
$.each(sigmaInstance.graph.nodes(), function(index, node){
if (node.degree < threshold){
return;
}
$.each(sigmaInstance.graph.adjacentNodes(node.id), function(index, anode){
if (anode.neo4j_data.name === appStore.endNode || anode.neo4j_data.name === appStore.startNode){
return;
}
var edges = sigmaInstance.graph.adjacentEdges(anode.id);
if ((edges.length > 1 || edges.length === 0) || (typeof anode.folded !== 'undefined')){
return;
}
var edge = edges[0];
if ((anode.type_user && (edge.label === 'MemberOf' || edge.label === 'AdminTo'))
|| (anode.type_computer && (edge.label === 'AdminTo' || edge.label === 'MemberOf'))
|| (anode.type_group && edge.label === 'AdminTo')){
if (typeof node.folded === 'undefined'){
node.folded = {}
node.folded.nodes = []
node.folded.edges = []
}
node.isGrouped = true
node.folded.nodes.push(anode)
node.folded.edges.push(edge)
appStore.spotlightData[anode.id] = [anode.neo4j_data.name, node.id, node.neo4j_data.name];
sigmaInstance.graph.dropNode(anode.id);
node.glyphs = [{
'position': 'bottom-left',
'content': node.folded.nodes.length
}]
}
});
})
return sigmaInstance
}
export function collapseSiblingNodes(sigmaInstance){
var threshold = appStore.performance.siblings
if (threshold === 0){
return;
}
$.each(sigmaInstance.graph.nodes(), function(index, node){
//Dont apply this logic to anything thats folded or isn't a computer
if (!node.type_computer || typeof node.folded !== 'undefined'){
return
}
//Start by getting all the edges attached to this node
var adjacent = sigmaInstance.graph.adjacentEdges(node.id)
var siblings = []
//Check to see if all the edges are the same type (i.e. AdminTo)
if (adjacent.length > 1 && adjacent.allEdgesSameType()){
//Get the "parents" by mapping the source from every edge
var parents = adjacent.map(
function(e){
return e.source
}
)
//Generate our string to compare other nodes to
//by sorting the parents and turning it into a string
var checkString = parents.sort().join(',')
var testString;
//Loop back over nodes in the graph and look for any nodes
//with identical parents
$.each(sigmaInstance.graph.nodes(), function(index, node2){
testString = sigmaInstance.graph.adjacentEdges(node2.id).map(
function(e){
return e.source;
}
).sort().join(',')
if (testString === checkString){
siblings.push(node);
}
});
if (siblings.length >= threshold){
//Generate a new ID for our grouped node
var nodeId = generateUniqueId(sigmaInstance, true);
var folded = {
nodes: [],
edges: []
}
sigmaInstance.graph.addNode({
id: nodeId,
x: node.x,
y: node.y,
degree: siblings.length,
label: "Grouped Computers",
neo4j_labels: ['Computer'],
neo4j_data: { name: "Grouped Computers" },
groupedNode: true,
glyphs: [{
position: 'bottom-left',
content: siblings.length
}]
});
//Generate new edges for each parent going to our new node
$.each(parents, function(index, parent){
var id = generateUniqueId(sigmaInstance, false);
sigmaInstance.graph.addEdge({
id: id,
source: parent,
target: nodeId,
label: 'AdminTo',
neo4j_type: 'AdminTo',
size: 1
})
})
var n = sigmaInstance.graph.nodes(nodeId);
//Loop over all the siblings, and push the edges into our new parent node
//Push the nodes in as well so we can unfold them
$.each(siblings, function(index, sibling){
$.each(sigmaInstance.graph.adjacentEdges(sibling.id), function(index, edge){
n.folded.edges.push(edge)
})
n.folded.nodes.push(sibling)
appStore.spotlightData[sibling.id] = [sibling.neo4j_data.name, nodeId, n.label];
sigmaInstance.graph.dropNode(sibling.id)
})
}
}
})
return sigmaInstance
}
export function generateUniqueId(sigmaInstance, isNode){
var i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10;
if (isNode){
while (sigmaInstance.graph.nodes(i) !== 'undefined'){
i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10;
}
}else{
while (sigmaInstance.graph.edges(i) !== 'undefined'){
i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10;
}
}
return i
}
export function defaultAjaxSettings(){
return {
url: appStore.databaseInfo.url + '/db/data/transaction/commit',
type: 'POST',
accepts: { json: 'application/json' },
dataType: 'json',
contentType: 'application/json',
headers: {
'Authorization': btoa(appStore.databaseInfo.user + ':' + appStore.databaseInfo.password)
}
}
}
export function fullAjax(statements, callback){
var options = defaultAjaxSettings();
options.success = callback;
if ($.isArray(statements)){
var x = []
$.each(statements, function(index, statement){
x.push({"statement": statemnt})
})
options.data = JSON.stringify({
"statements": x
})
}else{
options.data = JSON.stringify({
"statements": [{
"statement" : statements
}]
})
}
return options
}

768
test.html
View File

@ -1,768 +0,0 @@
<html>
<head>
<title>GraphTest</title>
<script src="js/jquery-2.2.1.min.js"></script>
<script src="js/jquery-ui.min.js"></script>
<link rel="stylesheet" type="text/css" href="css/font-awesome.min.css">
<link type="text/css" rel="stylesheet" href="css/bootstrap.min.css">
<link type="text/css" rel="stylesheet" href="css/bootstrap-theme.min.css">
<link rel="stylesheet" type="text/css" href="css/styles.css">
<link rel="stylesheet" type="text/css" href="css/animation.css">
<link rel="stylesheet" type="text/css" href="css/tooltip.css">
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<script type="text/javascript" src="js/sigma.min.js"></script>
<script type="text/javascript" src="js/plugins.min.js"></script>
<script type="text/javascript" src="js/bootstrap3-typeahead.min.js"></script>
<script type="text/javascript" src="js/mustache.min.js"></script>
<script type="text/javascript" src="js/bloodhound.js"></script>
<script type="text/javascript" src="js/sigma.renderers.linkurious.min.js"></script>
<script type="text/javascript" src="js/sigma.plugins.design.min.js"></script>
<script type="text/javascript" src="js/sigma.layouts.dagre.min.js"></script>
<script type="text/javascript" src="js/sigma.layouts.noverlap.js"></script>
<script type="text/javascript" src="js/sigma.renderers.glyphs.min.js"></script>
<script type="text/javascript" src="js/sigma.exporters.image.min.js"></script>
<script type="text/javascript" src="js/dagre.min.js"></script>
<style>
body{
padding-top:50px;
}
#graph{
height:100%;
}
.sigma-tooltip {
max-width: 300px;
max-height: 280px;
background-color: rgb(249, 247, 237);
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
border-radius: 6px;
}
.sigma-tooltip-header {
font-variant: small-caps;
font-size: 120%;
color: #437356;
border-bottom: 1px solid #aac789;
padding: 10px;
}
.sigma-tooltip-body {
padding: 10px;
}
.sigma-tooltip-body th {
color: #999;
text-align: left;
}
.sigma-tooltip-footer {
padding: 10px;
border-top: 1px solid #aac789;
}
.sigma-tooltip > .arrow {
border-width: 10px;
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.sigma-tooltip.top {
margin-top: -12px;
}
.sigma-tooltip.top > .arrow {
left: 50%;
bottom: -10px;
margin-left: -10px;
border-top-color: rgb(249, 247, 237);
border-bottom-width: 0;
}
.sigma-tooltip.bottom {
margin-top: 12px;
}
.sigma-tooltip.bottom > .arrow {
left: 50%;
top: -10px;
margin-left: -10px;
border-bottom-color: rgb(249, 247, 237);
border-top-width: 0;
}
.sigma-tooltip.left {
margin-left: -12px;
}
.sigma-tooltip.left > .arrow {
top: 50%;
right: -10px;
margin-top: -10px;
border-left-color: rgb(249, 247, 237);
border-right-width: 0;
}
.sigma-tooltip.right {
margin-left: 12px;
}
.sigma-tooltip.right > .arrow {
top: 50%;
left: -10px;
margin-top: -10px;
border-right-color: rgb(249, 247, 237);
border-left-width: 0;
}
.boxsize{
border: 3px solid black;
box-sizing: border-box;
background-color: white;
-moz-box-sizing: border-box;
padding-left: 10px;
margin-top:10px;
}
.lister dl{
padding: .5em;
}
.lister dt{
float: left;
clear: left;
width: 300px;
text-align: left;
font-weight:bold;
}
.lister dt:after{
content: ":";
}
.lister dd{
margin: 0 0 0 110px;
}
.loader {
margin: 100px auto;
font-size: 25px;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
-webkit-animation: load5 1.1s infinite ease;
animation: load5 1.1s infinite ease;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
@-webkit-keyframes load5 {
0%,
100% {
box-shadow: 0em -2.6em 0em 0em #C0C0C0, 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.5), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
}
12.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7), 1.8em -1.8em 0 0em #C0C0C0, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
}
25% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #C0C0C0, 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
37.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5), 2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
50% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.5), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #C0C0C0, -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
62.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5), 0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #C0C0C0, -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
75% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.5), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #C0C0C0, -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
87.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5), -2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #C0C0C0;
}
}
@keyframes load5 {
0%,
100% {
box-shadow: 0em -2.6em 0em 0em #C0C0C0, 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.5), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
}
12.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7), 1.8em -1.8em 0 0em #C0C0C0, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
}
25% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #C0C0C0, 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
37.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5), 2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
50% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.5), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #C0C0C0, -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
62.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5), 0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #C0C0C0, -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
75% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.5), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #C0C0C0, -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
87.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5), -2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #C0C0C0;
}
}transform:
</style>
<script type="text/javascript">
var queryStack = new Array();
var currentQuery = null;
var currentTooltip = null;
var descriptionTemplate = "{{#type_none}} \
<h3 align='center'>Node Properties</h3> \
<div style='padding-bottom:1em'>Select a node for more information</div> \
{{/type_none}} \
{{#type_loading}} \
<h3 align='center'>Loading...</h3> \
<div class='loader'>Loading...</div> \
{{/type_loading}} \
{{#type_user}} \
<h3 align='center'>{{label}} Properties</h2> \
<dl class='lister'>\
<dt>SAMAccountName</dt> \
<dd> None </dd> \
<dt>Display Name</dt> \
<dd> None </dd> \
<dt>Password Last Changed</dt> \
<dd> None </dd> \
<dt>First Degree Group Memberships</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Group),p=allShortestPaths((n)-[:MemberOf*1]->(target)) RETURN p')\">\
{{first_degree_group}}\
</a>\
</dd>\
<dt>Unrolled Group Memberships</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Group),p=allShortestPaths((n)-[:MemberOf*1..]->(target)) RETURN p')\">\
{{unrolled}}\
</a>\
</dd>\
<dt>First Degree Local Admin</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Computer), p=allShortestPaths((n)-[:AdminTo*1]->(target)) RETURN p')\">\
{{first_degree_admin}}\
</a>\
</dd>\
<dt>Group Delegated Local Admin Rights</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;})-[r:MemberOf]->(m:Group)-[s:AdminTo]->(l:Computer) RETURN n,r,m,s,l')\">\
{{dlar}}\
</a>\
</dd>\
<dt>Derivative Local Admin Rights</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Computer), p=allShortestPaths((n)-[*]->(target)) RETURN p')\">\
{{derivative}}\
</a>\
</dd>\
</dd>\
<dt>Sessions</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:Computer)-[r:HasSession]->(m:User {name:&quot;{{label}}&quot;}) RETURN n,r,m')\">\
{{sessions}}\
</a>\
</dd>\
</dl> \
{{/type_user}}\
{{#type_computer}} \
<h3 align='center'>{{label}} Properties</h2> \
<dl class='lister'>\
<dt>OS</dt> \
<dd> None </dd> \
<dt>Allows Unconstrained Delegation</dt> \
<dd> None </dd> \
<dt>Explicit Admins</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (a)-[b:AdminTo]->(c:Computer {name:&quot;{{label}}&quot;}) RETURN a,b,c')\">\
{{explicit_admins}}\
</a>\
</dd>\
<dt>Sessions</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (m:Computer {name:&quot;{{label}}&quot})-[r:HasSession]->(n:User) WITH n,r,m WHERE NOT n.name ENDS WITH &quot$&quot RETURN n,r,m')\">\
{{sessioncount}}\
</a>\
</dd>\
<dt>First Degree Local Admin</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Computer), p=allShortestPaths((n)-[:AdminTo*1]->(target)) RETURN p')\">\
{{first_degree_admin}}\
</a>\
</dd>\
<dt>Group Delegated Local Admin Rights</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;})-[r:MemberOf]->(m:Group)-[s:AdminTo]->(l:Computer) RETURN n,r,m,s,l')\">\
{{dlar}}\
</a>\
</dd>\
<dt>Derivative Local Admin Rights</dt>\
<dd>\
<a href=\"#\" onclick=\"doQuery('MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Computer), p=allShortestPaths((n)-[*]->(target)) RETURN p')\">\
{{derivative}}\
</a>\
</dd>\
</dl> \
{{/type_computer}}\
"
function redoLast(){
if (queryStack.length > 0){
if (currentTooltip != null){
currentTooltip.close();
}
sigma.layouts.stopForceLink();
currentQuery = queryStack.pop();
sigma.neo4j.cypher(
{url: 'http://localhost:7474', user:'neo4j', password:'neo4jj'},
currentQuery,
sigmaInstance,
function() {
$.each(sigmaInstance.graph.nodes(), function(index, node){
node.label = node.neo4j_data['name']
if (node.neo4j_labels[0]=='Group'){
node.type_group = true
node.color="#FF0000"
}else if (node.neo4j_labels[0] == 'User'){
node.type_user = true
node.color="#80FF00"
}else{
node.type_computer = true
node.color="#0000CC"
}
})
sigmaInstance.refresh();
sigma.layouts.startForceLink();
}
);
}
}
function doQuery(query){
queryStack.push(currentQuery);
currentQuery = query;
if (currentTooltip != null){
currentTooltip.close();
}
sigma.layouts.stopForceLink();
sigma.neo4j.cypher(
{url: 'http://localhost:7474', user:'neo4j', password:'neo4jj'},
query,
sigmaInstance,
function() {
$.each(sigmaInstance.graph.nodes(), function(index, node){
node.label = node.neo4j_data['name']
if (node.neo4j_labels[0]=='Group'){
node.type_group = true
node.color="#FF0000"
}else if (node.neo4j_labels[0] == 'User'){
node.type_user = true
node.color="#80FF00"
}else{
node.type_computer = true
node.color="#0000CC"
}
})
sigmaInstance.refresh();
sigma.layouts.startForceLink();
}
);
}
function loadDbData(){
$.ajax({
url: "http://localhost:7474/db/data/transaction/commit",
type: 'POST',
accepts: {json: "application/json"},
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": "Basic bmVvNGo6bmVvNGpq"
},
data: JSON.stringify({
"statements" : [ {
"statement" : "MATCH (n:User) WHERE NOT n.name ENDS WITH '$' RETURN count(n)"
}, {
"statement" : "MATCH (n:Group) RETURN count(n)"
}, {
"statement" : "MATCH ()-[r]->() RETURN count(r)"
}, {
"statement" : "MATCH (n:Computer) RETURN count(n)"
} ]
}),
success: function(json) {
var usercount = json.results[0].data[0].row[0];
var groupcount = json.results[1].data[0].row[0];
var relcount = json.results[2].data[0].row[0];
var compcount = json.results[3].data[0].row[0];
var template = $('#dbdata').html();
Mustache.parse(template);
var rendered = Mustache.render(template, {url:"http://localhost:7474/db/data/cypher", user: "neo4j", num_users: usercount, num_groups: groupcount, num_relationships: relcount, num_computers: compcount});
$('#dbdata').html(rendered);
}
});
}
function updateNodeData(node){
var rendered = Mustache.render(descriptionTemplate, {type_loading: true});
$('#nodeproperties').html(rendered);
if (node.data.node.type_user == true){
$.ajax({
url: "http://localhost:7474/db/data/transaction/commit",
type: 'POST',
accepts: {json: "application/json"},
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": "Basic bmVvNGo6bmVvNGpq"
},
data: JSON.stringify({
"statements" : [ {
"statement" : "MATCH (n:User {name:'" + node.data.node.label + "'}), (target:Group), p=allShortestPaths((n)-[:MemberOf*1]->(target)) RETURN count(target)"
}, {
"statement" : "MATCH (n:User {name:'" + node.data.node.label + "'}), (target:Group), p=allShortestPaths((n)-[:MemberOf*1..]->(target)) RETURN count(target)"
}, {
"statement" : "MATCH (n:User {name:'" + node.data.node.label + "'}), (target:Computer), p=allShortestPaths((n)-[:AdminTo*1]->(target)) RETURN count(target)"
}, {
"statement" : "MATCH (n:User {name:'" + node.data.node.label + "'})-[:MemberOf]->(m:Group)-[:AdminTo]->(l:Computer) RETURN count(l)"
}, {
"statement" : "MATCH (n:User {name:'" + node.data.node.label + "'}), (target:Computer), p=allShortestPaths((n)-[*]->(target)) RETURN count(target)"
}, {
"statement" : "MATCH (n:Computer)-[r:HasSession]->(m:User {name:'" + node.data.node.label + "'}) RETURN count(n)"
} ]
}),
success: function(json) {
var fdg = json.results[0].data[0].row[0];
var unr = json.results[1].data[0].row[0];
var diradmin = json.results[2].data[0].row[0];
var dla = json.results[3].data[0].row[0];
var dir = json.results[4].data[0].row[0];
var sessions = json.results[5].data[0].row[0];
var rendered = Mustache.render(descriptionTemplate, {label: node.data.node.label, first_degree_group:fdg, unrolled: unr, first_degree_admin: diradmin, type_user: true, dlar: dla, derivative:dir, sessions:sessions});
$('#nodeproperties').html(rendered);
}
});
}else if (node.data.node.type_computer == true){
$.ajax({
url: "http://localhost:7474/db/data/transaction/commit",
type: 'POST',
accepts: {json: "application/json"},
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": "Basic bmVvNGo6bmVvNGpq"
},
data: JSON.stringify({
"statements" : [ {
"statement" : "MATCH (a)-[b:AdminTo]->(c:Computer {name:'" + node.data.node.label + "'}) RETURN count(a)"
}, {
"statement" : "MATCH (m:Computer {name:'" + node.data.node.label + "'})-[r:HasSession]->(n:User) WITH n WHERE NOT n.name ENDS WITH '$' RETURN count(n)"
} ]
}),
success: function(json) {
var c1 = json.results[0].data[0].row[0];
var c2 = json.results[1].data[0].row[0];
var rendered = Mustache.render(descriptionTemplate, {label: node.data.node.label, explicit_admins: c1, type_computer: true, sessioncount: c2});
$('#nodeproperties').html(rendered);
}
});
}
}
function initNodeData(){
Mustache.parse(descriptionTemplate);
var rendered = Mustache.render(descriptionTemplate, {type_none: true});
$('#nodeproperties').html(rendered);
}
function setLabelAsStart(label){
$('#startpoint').val(label);
};
function setLabelAsEnd(label){
$('#endpoint').val(label);
};
jQuery(document).ready(function(){
sigma.renderers.def = sigma.renderers.canvas
sigmaInstance = new sigma({
container: 'graph'
});
sigmaInstance.settings({
edgeColor: 'default',
defaultEdgeColor: '#356',
nodeColor: 'default',
edgeHoverColor: 'default',
defaultEdgeHoverColor: '#090',
enableEdgeHovering: false,
defaultEdgeType: 'arrow',
minEdgeSize: 1,
maxEdgeSize: 2.5,
autoRescale: true
})
sigmaInstance.bind('clickNode', function(e){
updateNodeData(e)
})
loadDbData()
initNodeData()
var tooltips = sigma.plugins.tooltips(
sigmaInstance,
sigmaInstance.renderers[0],
{
node: [{
show: 'rightClickNode',
cssClass: 'sigma-tooltip',
template:
'<div class="arrow"></div>' +
' <div class="sigma-tooltip-header">{{label}}</div>' +
' <div class="sigma-tooltip-body">' +
' <div class="btn-group btn-group-justified" role="group" aria-label="...">' +
' {{#type_user}}'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="doQuery(\'MATCH (n {name:&quot;{{label}}&quot;})-[r:MemberOf]->m RETURN n,r,m\')">Members Of</button>' +
' </div>'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="doQuery(\'MATCH (n:User {name:&quot;{{label}}&quot;}), (target:Computer),p=allShortestPaths((n)-[*]->(target)) RETURN p\')">Admin To</button>' +
' </div>'+
' {{/type_user}}' +
' {{#type_computer}}' +
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="doQuery(\'MATCH (n {name:&quot;{{label}}&quot;})-[r:HasSession]->m WITH n,r,m WHERE NOT m.name ENDS WITH &quot;$&quot; RETURN n,r,m\')">Get Sessions</button>' +
' </div>'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="doQuery(\'MATCH (n {name:&quot;{{label}}&quot;})<-[r:AdminTo]-m RETURN n,r,m\')">Get Admins</button>' +
' </div>'+
' {{/type_computer}}' +
' {{#type_group}}' +
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="doQuery(\'MATCH (n {name:&quot;{{label}}&quot;})<-[r:MemberOf]-m RETURN n,r,m\')">Get Members</button>' +
' </div>'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="doQuery(\'MATCH (n {name:&quot;{{label}}&quot;})-[r:AdminTo]->m RETURN n,r,m\')">Admin To</button>' +
' </div>'+
' {{/type_group}}' +
' </div>' +
' <div class="btn-group btn-group-justified" role="group" aria-label="...">' +
' {{#type_user}}'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="setLabelAsStart(\'{{label}}\')">Set As Start Point</button>' +
' </div>'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="setLabelAsEnd(\'{{label}}\')">Set As End Point</button>' +
' </div>'+
' {{/type_user}}' +
' {{#type_computer}}' +
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="setLabelAsStart(\'{{label}}\')">Set As Start Point</button>' +
' </div>'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="setLabelAsEnd(\'{{label}}\')">Set As End Point</button>' +
' </div>'+
' {{/type_computer}}' +
' {{#type_group}}' +
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="setLabelAsStart(\'{{label}}\')">Set As Start Point</button>' +
' </div>'+
' <div class="btn-group" role="group">'+
' <button type="button" class="btn btn-default" onclick="setLabelAsEnd(\'{{label}}\')">Set As End Point</button>' +
' </div>'+
' {{/type_group}}' +
' </div>' +
' </div>' +
' <div class="sigma-tooltip-footer">Number of connections: {{degree}}</div>',
autoadjust: true,
renderer: function(node, template){
node.degree = this.degree(node.id)
return Mustache.render(template, node)
}
}]
}
)
tooltips.bind('shown', function(event){
currentTooltip = event.target;
})
tooltips.bind('hidden', function(event){
currentTooltip = null;
})
var fa = sigma.layouts.configForceLink(sigmaInstance, {
worker: true,
background: true,
scalingRatio: 5,
gravity: 1,
easing: 'cubicInOut',
autoStop: true,
alignNodeSiblings:true
});
fa.bind('start stop', function(event){
console.log(event.type);
})
$('#searchbar').bind('keypress', function(e){
if (e.which == 13){
doQuery('MATCH (n) WHERE n.name STARTS WITH "' + $('#searchbar').val() + '" AND NOT n.name ENDS WITH "$" RETURN n');
}
});
var dragListener = sigma.plugins.dragNodes(sigmaInstance, sigmaInstance.renderers[0])
$('#findpath').on('click', function(event){
doQuery("MATCH (source {name:'" + $('#startpoint').val() + "'}), (target {name:'" + $('#endpoint').val() + "'}), p=allShortestPaths((source)-[*]->(target)) RETURN p");
})
$('#backButton').on('click', function () {
redoLast()
});
$('#endpoint').typeahead({
source: function (query, process) {
return $.ajax({
url: "http://localhost:7474/db/data/cypher",
type: 'POST',
accepts: {json: "application/json"},
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": "Basic bmVvNGo6bmVvNGpq"
},
data: JSON.stringify( { "query" : "MATCH (n) WHERE n.name STARTS WITH '" + query + "' AND NOT n.name ENDS WITH '$' RETURN n.name LIMIT 10"}),
success: function(json) {
var d = json.data
var l = d.length;
for (var i = 0; i < l; i++) {
d[i] = d[i].toString();
}
return process(json.data);
}
});
}
});
$('#startpoint').typeahead({
source: function (query, process) {
return $.ajax({
url: "http://localhost:7474/db/data/cypher",
type: 'POST',
accepts: {json: "application/json"},
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": "Basic bmVvNGo6bmVvNGpq"
},
data: JSON.stringify( { "query" : "MATCH (n) WHERE n.name STARTS WITH '" + query + "' AND NOT n.name ENDS WITH '$' RETURN n.name LIMIT 25"}),
success: function(json) {
var d = json.data
var l = d.length;
for (var i = 0; i < l; i++) {
d[i] = d[i].toString();
}
return process(json.data);
}
});
}
});
$('#searchbar').typeahead({
source: function (query, process) {
return $.ajax({
url: "http://localhost:7474/db/data/cypher",
type: 'POST',
accepts: {json: "application/json"},
dataType: "json",
contentType: "application/json",
headers: {
"Authorization": "Basic bmVvNGo6bmVvNGpq"
},
data: JSON.stringify( { "query" : "MATCH (n) WHERE n.name STARTS WITH '" + query + "' AND NOT n.name ENDS WITH '$' RETURN n.name LIMIT 10"}),
success: function(json) {
var d = json.data
var l = d.length;
for (var i = 0; i < l; i++) {
d[i] = d[i].toString();
}
return process(json.data);
}
});
}, afterSelect: function(selected){
doQuery('MATCH (n) WHERE n.name STARTS WITH "' + selected + '" AND NOT n.name ENDS WITH "$" RETURN n');
}
});
doQuery("MATCH (n:Group {name:\'DOMAIN ADMINS\'})-[r]->(m) RETURN n,r,m");
});
</script>
</head>
<body role="document">
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Offensive Dashboard</a>
</div>
<div style="width:40%" class="navbar-form navbar-left">
<input id="searchbar" style="width:100%" autocomplete="off" type="text" class="form-control" placeholder="Start typing to search for a node...">
</div>
</div>
</nav>
<div class="jumbotron" style="padding-top:10px; height:80%" id="graph-container">
<div class="row">
<div class="col-md-7" id="graph" height="100%">
</div>
<div class="col-md-4" type="x-tmpl-mustache">
<div id="dbdata" class="boxsize" style="overflow:auto; width: 100%; padding-bottom: 10px">
<h4>
Connected to DB: {{url}} as {{user}}
</h4>
Users: {{num_users}}<br>
Computers: {{num_computers}}<br>
Groups: {{num_groups}}<br>
Relationships: {{num_relationships}}
</div>
<div id="nodeproperties" class="boxsize" style="overflow: auto;width:100%">
</div>
<div class="boxsize" style="height: 20%; width:100%;">
<h4 align="center">
Pre-Built Analytics Queries
</h4>
<a href="#" onclick="doQuery('MATCH (n), (target:Group {name:&quot;DOMAIN ADMINS&quot;}),p=allShortestPaths((n)-[*]->(target)) RETURN p')">Find Shortest Paths to DA</a>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Starting Node</span>
<input type="text" id="startpoint" class="form-control" data-provide="typeahead" autocomplete="off" aria-describedby="basic-addon1">
</div>
</div>
<div class="col-md-3">
<div class="input-group">
<span class="input-group-addon" id="basic-addon2">Ending Node</span>
<input type="text" id="endpoint" class="form-control" data-provide="typeahead" autocomplete="off" aria-describedby="basic-addon2">
</div>
</div>
<div class="col-md-3">
<button id="findpath" type="button" class="btn btn-block">
<span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span>Find Path
</button>
</div>
<div class="col-md-3">
<button id="backButton" type="button" class="btn btn-block">
<span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>Go Back
</button>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,31 @@
var webpack = require('webpack');
var webpackTargetElectronRenderer = require('webpack-target-electron-renderer');
var config = {
entry: [
'webpack-hot-middleware/client?reload=true&path=http://localhost:9000/__webpack_hmr',
'./src/index',
],
module: {
loaders: [{
test: /\.jsx?$/,
loaders: ['babel-loader'],
exclude: /node_modules/
}]
},
output: {
path: __dirname + '/dist',
publicPath: 'http://localhost:9000/dist/',
filename: 'bundle.js'
},
resolve: {
extensions: ['', '.js', '.jsx'],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
]
};
config.target = webpackTargetElectronRenderer(config);
module.exports = config;