diff --git a/BloodHoundExampleDB.graphdb/neostore.counts.db.a b/BloodHoundExampleDB.graphdb/neostore.counts.db.a index 98be43307..cda798b25 100644 Binary files a/BloodHoundExampleDB.graphdb/neostore.counts.db.a and b/BloodHoundExampleDB.graphdb/neostore.counts.db.a differ diff --git a/BloodHoundExampleDB.graphdb/neostore.transaction.db.0 b/BloodHoundExampleDB.graphdb/neostore.transaction.db.0 index 0fc9007e2..c76ae782f 100644 Binary files a/BloodHoundExampleDB.graphdb/neostore.transaction.db.0 and b/BloodHoundExampleDB.graphdb/neostore.transaction.db.0 differ diff --git a/index.html b/index.html index e8d0ef4e5..c35f12433 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,6 @@ <script src="node_modules/linkurious/dist/plugins.js" type="text/javascript"></script> <script src="node_modules/dagre/dist/dagre.min.js" type="text/javascript"></script> <script src="node_modules/bootstrap-3-typeahead/bootstrap3-typeahead.min.js" type="text/javascript"></script> - <script src="node_modules/neo4j-driver/lib/browser/neo4j-web.min.js" type="text/javascript"></script> <script src="src/js/papaparse.min.js" type="text/javascript"></script> <script src="src/js/simple-slider.min.js" type="text/javascript"></script> <link type="text/css" rel="stylesheet" href="src/css/simple-slider.css"> diff --git a/package.json b/package.json index f5784053e..6c1e00d21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bloodhound", - "version": "1.1.0", + "version": "1.2.0", "description": "Graph Theory for Active Directory", "keywords": [ "Graph", @@ -34,16 +34,16 @@ ] }, "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", - "concurrently": "^2.2.0", - "cross-env": "^2.0.0", - "electron": "^1.4.3", + "babel-cli": "^6.22.2", + "babel-core": "^6.22.1", + "babel-loader": "*", + "babel-polyfill": "^6.22.0", + "babel-preset-es2015": "^6.22.0", + "babel-preset-react": "^6.22.0", + "babel-preset-stage-0": "^6.22.0", + "concurrently": "^3.1.0", + "cross-env": "^3.1.4", + "electron": "^1.4.15", "express": "^4.14.0", "webpack": "^1.13.1", "webpack-dev-middleware": "^1.6.1", @@ -51,19 +51,20 @@ "webpack-target-electron-renderer": "^0.4.0" }, "dependencies": { + "async": "^2.1.4", "bootstrap": "^3.3.6", "bootstrap-3-typeahead": "^4.0.1", - "configstore": "^2.0.0", + "configstore": "^2.1.0", "dagre": "^0.7.4", - "eventemitter2": "^2.0.0", + "eventemitter2": "^2.2.2", "jquery": "^2.2.4", "linkurious": "^1.5.1", "mustache": "^2.2.1", - "neo4j-driver": "^1.0.4", - "react": "^15.3.1", - "react-addons-css-transition-group": "^15.3.1", + "neo4j-driver": "^1.1.0", + "react": "^15.4.2", + "react-addons-css-transition-group": "^15.4.2", "react-bootstrap": "^0.30.3", - "react-dom": "^15.3.1", + "react-dom": "^15.4.2", "react-if": "^2.1.0" } } diff --git a/src/AppContainer.jsx b/src/AppContainer.jsx index a43e350d9..17fd0c73f 100644 --- a/src/AppContainer.jsx +++ b/src/AppContainer.jsx @@ -15,7 +15,9 @@ import ExportContainer from './components/Float/ExportContainer'; import Settings from './components/Float/Settings' import ZoomContainer from './components/Zoom/ZoomContainer' import QueryNodeSelect from './components/Float/QueryNodeSelect' +import SessionClearModal from './components/Modals/SessionClearModal' import ReactCSSTransitionGroup from 'react-addons-css-transition-group' +import About from './components/Modals/About.jsx' export default class AppContainer extends Component { constructor(){ @@ -41,11 +43,13 @@ export default class AppContainer extends Component { <ClearConfirmModal /> <ClearingModal /> <CancelUploadModal /> + <SessionClearModal /> <RawQuery /> <MenuContainer /> <Settings /> <ZoomContainer /> <QueryNodeSelect /> + <About /> </div> </ReactCSSTransitionGroup> ); diff --git a/src/components/Float/Login.jsx b/src/components/Float/Login.jsx index 7c5120ead..05f6e1d5e 100644 --- a/src/components/Float/Login.jsx +++ b/src/components/Float/Login.jsx @@ -7,8 +7,6 @@ export default class Login extends Component { url: "", icon: null, loginEnabled: false, - dbHelpVisible: false, - loginHelpVisible: false, user: "", password: "", loginInProgress: false @@ -18,6 +16,8 @@ export default class Login extends Component { checkDBPresence(){ var url = this.state.url; var icon = this.state.icon; + var jicon = jQuery(icon) + var btn = jQuery(this.refs.loginButton) if (url === ""){ return; @@ -38,28 +38,41 @@ export default class Login extends Component { icon.removeClass(); icon.addClass("fa fa-spinner fa-spin form-control-feedback"); icon.toggle(true); - var driver = neo4j.v1.driver(url) - var session = driver.session() - - session.run('MATCH (n) RETURN (n) LIMIT 1') - .subscribe({ - onNext: function(next){ - }, - onError: function(error){ - if (error.code){ - this.setState({dbHelpVisible: true}) - icon.removeClass(); - icon.addClass("fa fa-times-circle red-icon-color form-control-feedback"); - }else{ - icon.removeClass(); - icon.addClass("fa fa-check-circle green-icon-color form-control-feedback"); - this.setState({loginEnabled: true, url: url}) - } - }.bind(this), - onComplete: function(){ - session.close() - } - }) + var driver = neo4j.driver(url) + driver.onCompleted = function(){ + driver.close() + } + driver.onError = function(error){ + console.log(error) + if (error.message && error.message.includes("encryption certificate has changed")){ + var path = error.message.match("`(.*?)`")[1] + icon.removeClass(); + icon.addClass("fa fa-times-circle red-icon-color form-control-feedback"); + icon.attr('data-original-title', 'Certificate error - delete localhost line in {}'.format(path)) + .tooltip('fixTitle') + .tooltip('show') + this.setState({ + loginInProgress: false, + loginEnabled: false + }) + }else if (error.fields && error.fields[0].code === "Neo.ClientError.Security.Unauthorized"){ + icon.removeClass(); + icon.addClass("fa fa-check-circle green-icon-color form-control-feedback"); + this.setState({loginEnabled: true, url: url}) + }else{ + icon.removeClass(); + icon.addClass("fa fa-times-circle red-icon-color form-control-feedback"); + icon.attr('data-original-title', 'No database found') + .tooltip('fixTitle') + .tooltip('show') + this.setState({ + loginInProgress: false, + loginEnabled: false + }) + } + driver.close() + }.bind(this) + driver.session(); } checkDBCreds(){ @@ -68,24 +81,80 @@ export default class Login extends Component { } this.setState({ loginInProgress: true, - loginHelpVisible: false, loginEnabled: false }) var btn = jQuery(this.refs.loginButton) + var pwf = jQuery(this.refs.password) - var driver = neo4j.v1.driver(this.state.url, neo4j.v1.auth.basic(this.state.user, this.state.password),{knownHosts: 'known_hosts'}) - var session = driver.session() + var driver = neo4j.driver(this.state.url, neo4j.auth.basic(this.state.user, this.state.password)) + driver.onError = function(error){ + if (error.fields && error.fields[0].code === "Neo.ClientError.Security.Unauthorized"){ + btn.removeClass('activate'); + this.setState({ + loginInProgress: false, + loginEnabled: true + }) + pwf.attr('data-original-title', 'Invalid username or password') + .tooltip('fixTitle') + .tooltip('show') + }else if (error.fields && error.fields[0].code === "Neo.ClientError.Security.AuthenticationRateLimit"){ + btn.removeClass('activate'); + this.setState({ + loginInProgress: false, + loginEnabled: true + }) + pwf.attr('data-original-title', 'Too many authentication attempts, please wait') + .tooltip('fixTitle') + .tooltip('show') + }else if (error.message && error.message.includes("encryption certificate has changed")){ + var path = error.message.match("`(.*?)`")[1] + var icon = this.state.icon + icon.toggle('true') + icon.removeClass(); + icon.addClass("fa fa-times-circle red-icon-color form-control-feedback"); + jQuery(icon).tooltip({ + placement : 'right', + title: 'Certificate error - delete localhost line in ' + path, + container: 'body', + delay: {show: 200, hide: 0}, + template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner tooltip-inner-custom"></div></div>' + }) + this.setState({ + loginInProgress: false, + loginEnabled: false + }) + jQuery(icon).tooltip('show') + }else if (error.toString().includes('ECONNREFUSED')){ + var icon = this.state.icon + icon.toggle('true') + icon.removeClass(); + icon.addClass("fa fa-times-circle red-icon-color form-control-feedback"); + icon.attr('data-original-title', 'No database found') + .tooltip('fixTitle') + .tooltip('show') + this.setState({ + loginInProgress: false, + loginEnabled: false + }) + } + driver.close() + }.bind(this) + var session = driver.session(); session.run('MATCH (n) RETURN (n) LIMIT 1') .subscribe({ onError: function(error){ - btn.toggleClass('activate'); - this.setState({ - loginHelpVisible: true, - loginInProgress: false, - loginEnabled: true - }) - + btn.removeClass('activate'); + var url = this.state.url.replace('bolt://','http://').replace('7687','7474') + if (error.fields && error.fields[0].code === "Neo.ClientError.Security.CredentialsExpired"){ + pwf.attr('data-original-title', 'Credentials need to be changed from the neo4j browser first. Go to {} and change them.'.format(url)) + .tooltip('fixTitle') + .tooltip('show') + this.setState({ + loginInProgress: false, + loginEnabled: true + }) + } }.bind(this), onNext: function(){ @@ -103,14 +172,16 @@ export default class Login extends Component { user: this.state.user, password: this.state.password }) - global.driver = driver appStore.databaseInfo = conf.get('databaseInfo'); + jQuery(this.refs.password).tooltip('hide') + jQuery(this.refs.urlspinner).tooltip('hide') setTimeout(function(){ jQuery(this.refs.outer).fadeOut(400, function(){ renderEmit.emit('login'); }); }.bind(this), 1500) - session.close() + driver.close() + global.driver = neo4j.driver(this.state.url, neo4j.auth.basic(this.state.user, this.state.password)) }.bind(this) }) @@ -130,8 +201,23 @@ export default class Login extends Component { } componentDidMount() { - jQuery(this.refs.urlspinner).toggle(false) + jQuery(this.refs.password).tooltip({ + placement : 'right', + title: '', + container: 'body', + trigger: 'manual', + template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner tooltip-inner-custom"></div></div>' + }) this.setState({icon: jQuery(this.refs.urlspinner)}) + var icon = jQuery(this.refs.urlspinner) + icon.tooltip({ + placement : 'right', + title: '', + container: 'body', + delay: {show: 200, hide: 0}, + template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner tooltip-inner-custom"></div></div>' + }) + icon.toggle(false) if (this.state.password !== ""){ this.checkDBCreds(); } @@ -142,11 +228,13 @@ export default class Login extends Component { } _userChanged(event){ - this.setState({user: event.target.value}) + this.setState({user: event.target.value}) + jQuery(this.refs.password).tooltip('hide') } _passChanged(event){ - this.setState({password: event.target.value}) + this.setState({password: event.target.value}) + jQuery(this.refs.password).tooltip('hide') } _triggerLogin(e){ @@ -171,10 +259,9 @@ export default class Login extends Component { <span className="input-group-addon" id="dburladdon"> Database URL </span> - <input ref="url" onFocus={function(){this.setState({dbHelpVisible: false})}.bind(this)} onBlur={this.checkDBPresence.bind(this)} onChange={this._urlChanged.bind(this)} type="text" className="form-control" value={this.state.url} placeholder="bolt://localhost:7687" aria-describedby="dburladdon" /> + <input ref="url" onFocus={function(){jQuery(this.state.icon).tooltip('hide');}.bind(this)} onBlur={this.checkDBPresence.bind(this)} onChange={this._urlChanged.bind(this)} type="text" className="form-control" value={this.state.url} placeholder="bolt://localhost:7687" aria-describedby="dburladdon" /> <i ref="urlspinner" className="fa fa-spinner fa-spin form-control-feedback" /> </div> - {this.state.dbHelpVisible ? <p className="help-block help-block-add">No Neo4j Database Found</p> : null} <div className="input-group spacing"> <span className="input-group-addon" id="dbuseraddon">DB Username</span> <input ref="user" type="text" value={this.state.user} onKeyUp={this._triggerLogin.bind(this)} onChange={this._userChanged.bind(this)} className="form-control" placeholder="neo4j" aria-describedby="dbuseraddon" /> @@ -183,7 +270,6 @@ export default class Login extends Component { <span className="input-group-addon" id="dbpwaddon">DB Password</span> <input ref="password" value={this.state.password} onKeyDown={this._triggerLogin.bind(this)} onChange={this._passChanged.bind(this)} type="password" className="form-control" placeholder="neo4j" aria-describedby="dbpwaddon" /> </div> - {this.state.loginHelpVisible ? <p className="help-block help-block-add" style={{color: "#d9534f"}}>Wrong username or password</p> : null} <button ref="loginButton" disabled={!this.state.loginEnabled} type="button" onClick={this.checkDBCreds.bind(this)} className="btn btn-primary loginbutton has-spinner"> Login <span className="button-spinner"> diff --git a/src/components/Graph.jsx b/src/components/Graph.jsx index 61c1515f8..693cf2e4e 100644 --- a/src/components/Graph.jsx +++ b/src/components/Graph.jsx @@ -209,6 +209,7 @@ export default class GraphContainer extends Component { clearGraph(){ this.state.sigmaInstance.graph.clear() + this.state.sigmaInstance.refresh() } setGraphicsMode(){ @@ -764,8 +765,10 @@ export default class GraphContainer extends Component { }); dagreListener.bind('stop', function(event){ - emitter.emit('updateLoadingText', "Fixing Overlap"); - sigmaInstance.startNoverlap(); + emitter.emit('updateLoadingText', 'Done!'); + setTimeout(function(){ + emitter.emit('showLoadingIndicator', false); + }, 1500) }) dagreListener.bind('start', function(event){ diff --git a/src/components/Menu/MenuContainer.jsx b/src/components/Menu/MenuContainer.jsx index aac42e0b4..391707f52 100644 --- a/src/components/Menu/MenuContainer.jsx +++ b/src/components/Menu/MenuContainer.jsx @@ -1,10 +1,11 @@ import React, { Component } from 'react'; import MenuButton from './MenuButton'; import ProgressBarMenuButton from './ProgressBarMenuButton'; -import { buildDomainProps, buildSessionProps, buildLocalAdminProps, buildGroupMembershipProps } from 'utils'; +import { buildDomainProps, buildSessionProps, buildLocalAdminProps, buildGroupMembershipProps, buildACLProps } from 'utils'; import { If, Then, Else } from 'react-if'; const { dialog, clipboard } = require('electron').remote var fs = require('fs') +var async = require('async') export default class MenuContainer extends Component { constructor(){ @@ -51,136 +52,198 @@ export default class MenuContainer extends Component { } } + _settingsClick(){ + emitter.emit('openSettings') + } + + _cancelUploadClick(){ + emitter.emit('showCancelUpload') + } + _uploadClick(){ - var filename = dialog.showOpenDialog({ - properties: ['openFile'] - })[0] + var input = jQuery(this.refs.fileInput) + var files = $.makeArray(input[0].files) + + async.eachSeries(files, function(file, callback){ + emitter.emit('showAlert', 'Processing file {}'.format(file.name)); + this.processFile(file.path, file, callback) + }.bind(this), + function done(){ + setTimeout(function(){ + this.setState({uploading: false}) + }.bind(this), 3000) + }.bind(this)) + + input.val('') + } + + _aboutClick(){ + emitter.emit('showAbout') + } + + processFile(filename, fileobject, callback){ var sent = 0 - fs.readFile(filename, 'utf8', function(err, data){ - var count = data.split('\n').length; - var header = data.split('\n')[0] - var filetype; - if (header.includes('UserName') && header.includes('ComputerName') && header.includes('Weight')){ - filetype = 'sessions' - }else if (header.includes('AccountName') && header.includes('AccountType') && header.includes('GroupName')){ - filetype = 'groupmembership' - }else if (header.includes('AccountName') && header.includes('AccountType') && header.includes('ComputerName')){ - filetype = 'localadmin' - }else if (header.includes('SourceDomain') && header.includes('TargetDomain') && header.includes('TrustDirection') && header.includes('TrustType') && header.includes('Transitive')){ - filetype = 'domain' - } - - if (typeof filetype === 'undefined'){ - emitter.emit('showAlert', 'Unrecognized CSV Type'); - return; - } - - this.setState({ - uploading: true, - progress: 0 - }) - Papa.parse(data,{ - header: true, - dynamicTyping: true, - chunkSize: 50000, - skipEmptyLines: true, - chunk: function(rows, parser){ - this.setState({parser: parser}) - if (rows.data.length === 0){ - parser.abort() - this.setState({progress:100}) - setTimeout(function(){ - this.setState({uploading: false}) - }.bind(this), 3000) - emitter.emit('refreshDBData') - return + var i; + var count = 0; + var header = "" + var procHeader = true; + fs.createReadStream(filename) + .on('data', function(chunk) { + for (i=0; i < chunk.length; ++i){ + if (procHeader){ + header = header + String.fromCharCode(chunk[i]) } - parser.pause() - sent += rows.data.length - if (filetype === 'sessions'){ - var query = 'UNWIND {props} AS prop MERGE (user:User {name:prop.account}) WITH user,prop MERGE (computer:Computer {name: prop.computer}) WITH user,computer,prop MERGE (computer)-[:HasSession {Weight : prop.weight}]-(user)' - var props = buildSessionProps(rows.data) - var session = driver.session() - session.run(query, {props: props}) - .then(function(){ - this.setState({progress: Math.floor((sent / count) * 100)}) - session.close() - parser.resume() - }.bind(this)) - }else if (filetype === 'groupmembership'){ - var props = buildGroupMembershipProps(rows.data) - var userQuery = 'UNWIND {props} AS prop MERGE (user:User {name:prop.account}) WITH user,prop MERGE (group:Group {name:prop.group}) WITH user,group MERGE (user)-[:MemberOf]->(group)' - var computerQuery = 'UNWIND {props} AS prop MERGE (computer:Computer {name:prop.account}) WITH computer,prop MERGE (group:Group {name:prop.group}) WITH computer,group MERGE (computer)-[:MemberOf]->(group)' - var groupQuery = 'UNWIND {props} AS prop MERGE (group1:Group {name:prop.account}) WITH group1,prop MERGE (group2:Group {name:prop.group}) WITH group1,group2 MERGE (group1)-[:MemberOf]->(group2)' - var s1 = driver.session() - var s2 = driver.session() - var s3 = driver.session() - var p1 - var p2 - var p3 - p1 = s1.run(userQuery, {props: props.users}) - p1.then(function(){ - s1.close() - p2 = s2.run(computerQuery, {props: props.computers}) - p2.then(function(){ - s2.close() - p3 = s3.run(groupQuery, {props: props.groups}) - p3.then(function(){ - s3.close() + if (chunk[i] == 10){ + if (procHeader){ + procHeader = false; + } + count++ + }; + } + + }) + .on('end', function() { + count = count - 1 + var filetype; + if (header.includes('UserName') && header.includes('ComputerName') && header.includes('Weight')){ + filetype = 'sessions' + }else if (header.includes('AccountName') && header.includes('AccountType') && header.includes('GroupName')){ + filetype = 'groupmembership' + }else if (header.includes('AccountName') && header.includes('AccountType') && header.includes('ComputerName')){ + filetype = 'localadmin' + }else if (header.includes('SourceDomain') && header.includes('TargetDomain') && header.includes('TrustDirection') && header.includes('TrustType') && header.includes('Transitive')){ + filetype = 'domain' + }else if (header.includes('ActiveDirectoryRights') && header.includes('ObjectType') && header.includes('PrincipalType')){ + filetype = 'acl' + } + + if (typeof filetype === 'undefined'){ + emitter.emit('showAlert', 'Unrecognized CSV Type'); + return; + } + + this.setState({ + uploading: true, + progress: 0 + }) + //I have no idea why this workaround is needed. Apparently all my sessions freeze unless I make a random query + setTimeout(function(){ + var sess = driver.session() + sess.run('MATCH (n) RETURN (n) LIMIT 1') + .then(function(){ + sess.close() + }) + }, 1000) + + console.time('IngestTime') + Papa.parse(fileobject,{ + header: true, + dynamicTyping: true, + skipEmptyLines: true, + chunkSize: 5242880, + //chunkSize: 500000, + chunk: function(rows, parser){ + this.setState({parser: parser}) + if (rows.data.length === 0){ + console.timeEnd('IngestTime') + parser.abort() + this.setState({progress:100}) + emitter.emit('refreshDBData') + callback() + return + } + parser.pause() + sent += rows.data.length + if (filetype === 'sessions'){ + var query = 'UNWIND {props} AS prop MERGE (user:User {name:prop.account}) WITH user,prop MERGE (computer:Computer {name: prop.computer}) WITH user,computer,prop MERGE (computer)-[:HasSession {Weight : prop.weight}]-(user)' + var props = buildSessionProps(rows.data) + var session = driver.session() + session.run(query, {props: props}) + .then(function(){ this.setState({progress: Math.floor((sent / count) * 100)}) + session.close() parser.resume() }.bind(this)) - }.bind(this)) - }.bind(this)) - }else if (filetype === 'localadmin'){ - var props = buildLocalAdminProps(rows.data) - var userQuery = 'UNWIND {props} AS prop MERGE (user:User {name: prop.account}) WITH user,prop MERGE (computer:Computer {name: prop.computer}) WITH user,computer MERGE (user)-[:AdminTo]->(computer)' - var groupQuery = 'UNWIND {props} AS prop MERGE (group:Group {name: prop.account}) WITH group,prop MERGE (computer:Computer {name: prop.computer}) WITH group,computer MERGE (group)-[:AdminTo]->(computer)' - var computerQuery = 'UNWIND {props} AS prop MERGE (computer1:Computer {name: prop.account}) WITH computer1,prop MERGE (computer2:Computer {name: prop.computer}) WITH computer1,computer2 MERGE (computer1)-[:AdminTo]->(computer2)' - - var s1 = driver.session() - var s2 = driver.session() - var s3 = driver.session() - var p1 - var p2 - var p3 - p1 = s1.run(userQuery, {props: props.users}) - p1.then(function(){ - s1.close() - p2 = s2.run(computerQuery, {props: props.computers}) - p2.then(function(){ - s2.close() - p3 = s3.run(groupQuery, {props: props.groups}) - p3.then(function(){ - s3.close() + }else if (filetype === 'groupmembership'){ + var props = buildGroupMembershipProps(rows.data) + var userQuery = 'UNWIND {props} AS prop MERGE (user:User {name:prop.account}) WITH user,prop MERGE (group:Group {name:prop.group}) WITH user,group MERGE (user)-[:MemberOf]->(group)' + var computerQuery = 'UNWIND {props} AS prop MERGE (computer:Computer {name:prop.account}) WITH computer,prop MERGE (group:Group {name:prop.group}) WITH computer,group MERGE (computer)-[:MemberOf]->(group)' + var groupQuery = 'UNWIND {props} AS prop MERGE (group1:Group {name:prop.account}) WITH group1,prop MERGE (group2:Group {name:prop.group}) WITH group1,group2 MERGE (group1)-[:MemberOf]->(group2)' + + var session = driver.session() + var tx = session.beginTransaction() + var promises = [] + + promises.push(tx.run(userQuery, {props: props.users})) + promises.push(tx.run(computerQuery, {props: props.computers})) + promises.push(tx.run(groupQuery, {props: props.groups})) + + Promise.all(promises) + .then(function(){ + tx.commit() + .then(function(){ + session.close() + this.setState({progress: Math.floor((sent / count) * 100)}) + parser.resume() + }.bind(this)) + }.bind(this)) + }else if (filetype === 'localadmin'){ + var props = buildLocalAdminProps(rows.data) + var userQuery = 'UNWIND {props} AS prop MERGE (user:User {name: prop.account}) WITH user,prop MERGE (computer:Computer {name: prop.computer}) WITH user,computer MERGE (user)-[:AdminTo]->(computer)' + var groupQuery = 'UNWIND {props} AS prop MERGE (group:Group {name: prop.account}) WITH group,prop MERGE (computer:Computer {name: prop.computer}) WITH group,computer MERGE (group)-[:AdminTo]->(computer)' + var computerQuery = 'UNWIND {props} AS prop MERGE (computer1:Computer {name: prop.account}) WITH computer1,prop MERGE (computer2:Computer {name: prop.computer}) WITH computer1,computer2 MERGE (computer1)-[:AdminTo]->(computer2)' + + var session = driver.session() + var tx = session.beginTransaction() + var promises = [] + + promises.push(tx.run(userQuery, {props: props.users})) + promises.push(tx.run(computerQuery, {props: props.computers})) + promises.push(tx.run(groupQuery, {props: props.groups})) + + Promise.all(promises) + .then(function(){ + tx.commit() + .then(function(){ + session.close() + this.setState({progress: Math.floor((sent / count) * 100)}) + parser.resume() + }.bind(this)) + }.bind(this)) + }else if (filetype === 'domain'){ + var props = buildDomainProps(rows.data) + var query = "UNWIND {props} AS prop MERGE (domain1:Domain {name: prop.domain1}) WITH domain1,prop MERGE (domain2:Domain {name: prop.domain2}) WITH domain1,domain2,prop MERGE (domain1)-[:TrustedBy {TrustType : prop.trusttype, Transitive: prop.transitive}]->(domain2)" + var session = driver.session() + session.run(query, {props: props}) + .then(function(){ this.setState({progress: Math.floor((sent / count) * 100)}) + session.close() parser.resume() }.bind(this)) - }.bind(this)) - }.bind(this)) - }else{ - var props = buildDomainProps(rows.data) - var query = "UNWIND {props} AS prop MERGE (domain1:Domain {name: prop.domain1}) WITH domain1,prop MERGE (domain2:Domain {name: prop.domain2}) WITH domain1,domain2,prop MERGE (domain1)-[:TrustedBy {TrustType : prop.trusttype, Transitive: prop.transitive}]->(domain2)" - var session = driver.session() - session.run(query, {props: props}) - .then(function(){ - this.setState({progress: Math.floor((sent / count) * 100)}) - session.close() - parser.resume() - }.bind(this)) - } - }.bind(this) - }) - }.bind(this)) - } - - _settingsClick(){ - emitter.emit('openSettings') - } + }else if (filetype === 'acl'){ + var data = buildACLProps(rows.data) + var promises = [] + var session = driver.session() + var tx = session.beginTransaction() + for (var key in data){ + var promise = tx.run(data[key].statement, {props: data[key].props}) + promises.push(promise) + } - _cancelUploadClick(){ - emitter.emit('showCancelUpload') + Promise.all(promises) + .then(function(){ + tx.commit() + .then(function(){ + this.setState({progress: Math.floor((sent / count) * 100)}) + session.close() + parser.resume() + }.bind(this)) + }.bind(this)) + } + }.bind(this) + }) + }.bind(this)); } render() { @@ -201,7 +264,7 @@ export default class MenuContainer extends Component { <ProgressBarMenuButton click={this._cancelUploadClick.bind(this)} progress={this.state.progress} committed={this.state.committed}/> </Then> <Else>{ () => - <MenuButton click={this._uploadClick.bind(this)} hoverVal="Upload Data" glyphicon="glyphicon glyphicon-upload" /> + <MenuButton click={function(){jQuery(this.refs.fileInput).click()}.bind(this)} hoverVal="Upload Data" glyphicon="glyphicon glyphicon-upload" /> }</Else> </If> </div> @@ -211,6 +274,10 @@ export default class MenuContainer extends Component { <div> <MenuButton click={this._settingsClick.bind(this)} hoverVal="Settings" glyphicon="fa fa-cogs" /> </div> + <div> + <MenuButton click={this._aboutClick.bind(this)} hoverVal="About" glyphicon="fa fa-info" /> + </div> + <input ref="fileInput" multiple className="hide" type="file" onChange={this._uploadClick.bind(this)}/> </div> ); } diff --git a/src/components/Modals/About.jsx b/src/components/Modals/About.jsx new file mode 100644 index 000000000..ab06c2397 --- /dev/null +++ b/src/components/Modals/About.jsx @@ -0,0 +1,67 @@ +import React, {Component} from 'react'; + +var Modal = require('react-bootstrap').Modal; +var path = require('path') +var fs = require('fs') +const { app, shell } = require('electron').remote + + +export default class componentName extends Component { + constructor(){ + super(); + + var json = JSON.parse(fs.readFileSync(path.join(app.getAppPath(),'package.json'))); + + fs.readFile(path.join(app.getAppPath(),'LICENSE.md'), 'utf8', function(err, data){ + this.setState({ + license: data + }) + }.bind(this)); + + this.state = { + open: false, + version: json.version + } + } + + closeModal(){ + this.setState({ open: false }) + } + + openModal(){ + this.setState({open: true}) + } + + componentDidMount() { + emitter.on('showAbout', this.openModal.bind(this)) + } + + render() { + return ( + <Modal + show={this.state.open} + onHide={this.closeModal.bind(this)} + aria-labelledby="AboutHeader"> + + <Modal.Header closeButton={true}> + <Modal.Title id="AboutHeader">About BloodHound</Modal.Title> + </Modal.Header> + + <Modal.Body> + <h5><b>Version:</b> {this.state.version}</h5> + <h5><b>Github:</b> <a href="#" onClick={function(){shell.openExternal("https://www.github.com/adaptivethreat/BloodHound")}}>https://www.github.com/adaptivethreat/BloodHound</a></h5> + <h5><b>Authors:</b> <a href="#" onClick={function(){shell.openExternal("https://www.twitter.com/harmj0y")}}>@harmj0y</a>, <a href="#" onClick={function(){shell.openExternal("https://www.twitter.com/_wald0")}}>@_wald0</a>, <a href="#" onClick={function(){shell.openExternal("https://www.twitter.com/cptjesus")}}>@cptjesus</a></h5> + <br /> + <h5><b>License</b></h5> + <div className="aboutscroll">{this.state.license}</div> + </Modal.Body> + + <Modal.Footer> + <button type="button" className="btn btn-primary" onClick={this.closeModal.bind(this)}> + Close + </button> + </Modal.Footer> + </Modal> + ); + } +} \ No newline at end of file diff --git a/src/components/Modals/ClearingModal.jsx b/src/components/Modals/ClearingModal.jsx index 8911097fd..d2e2941ad 100644 --- a/src/components/Modals/ClearingModal.jsx +++ b/src/components/Modals/ClearingModal.jsx @@ -30,7 +30,7 @@ export default class ClearingModal extends Component { aria-labelledby="ClearingModalHeader"> <Modal.Header closeButton={true}> - <Modal.Title id="ClearingModalHeader">Clearing Database</Modal.Title> + <Modal.Title id="ClearingModalHeader">Clearing Data</Modal.Title> </Modal.Header> </Modal> ); diff --git a/src/components/Modals/LogoutModal.jsx b/src/components/Modals/LogoutModal.jsx index d7b11185e..82a88ae30 100644 --- a/src/components/Modals/LogoutModal.jsx +++ b/src/components/Modals/LogoutModal.jsx @@ -23,8 +23,7 @@ export default class LogoutModal extends Component { this.setState({ open: false }) emitter.emit('doLogout'); driver.close() - ReactDOM.unmountComponentAtNode(document.getElementById('root')) - ReactDOM.render(<Login />, document.getElementById('root')) + renderEmit.emit('logout') } openModal(){ diff --git a/src/components/Modals/SessionClearModal.jsx b/src/components/Modals/SessionClearModal.jsx new file mode 100644 index 000000000..dcaf8f9fc --- /dev/null +++ b/src/components/Modals/SessionClearModal.jsx @@ -0,0 +1,54 @@ +import React, { Component } from 'react'; +import { clearSessions } from 'utils' + +var Modal = require('react-bootstrap').Modal; + +export default class SessionClearModal extends Component { + constructor(){ + super(); + + this.state = { + open: false + } + } + + closeModal(){ + this.setState({open: false}) + } + + openModal(){ + this.setState({open: true}) + } + + closeAndClear(){ + this.setState({ open: false }) + clearSessions(); + } + + componentDidMount(){ + emitter.on('openSessionClearModal', this.openModal.bind(this)) + } + + render() { + return ( + <Modal + show={this.state.open} + onHide={this.closeModal.bind(this)} + aria-labelledby="SessionModalHeader"> + + <Modal.Header closeButton={true}> + <Modal.Title id="SessionModalHeader">Clear Sessions</Modal.Title> + </Modal.Header> + + <Modal.Body> + <p>Are you sure you want to clear sessions?</p> + </Modal.Body> + + <Modal.Footer> + <button onClick={this.closeAndClear.bind(this)} type="button" className="btn btn-danger">Clear Sessions</button> + <button onClick={this.closeModal.bind(this)} type="button" className="btn btn-primary">Cancel</button> + </Modal.Footer> + </Modal> + ); + } +} diff --git a/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx b/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx index f350e4f94..3da5a5a7d 100644 --- a/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx +++ b/src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx @@ -43,6 +43,10 @@ export default class DatabaseDataDisplay extends Component { toggleDBWarnModal(){ emitter.emit('openDBWarnModal') } + + toggleSessionClearModal(){ + emitter.emit('openSessionClearModal') + } render() { @@ -67,8 +71,9 @@ export default class DatabaseDataDisplay extends Component { </dl> <div className="text-center"> - <div className="btn-group dbbuttons"> + <div className="btn-group btn-group-sm dbbuttons"> <button type="button" className="btn btn-success" onClick={function(){this.refreshDBData()}.bind(this)}>Refresh DB Stats</button> + <button type="button" className="btn btn-info" onClick={this.toggleSessionClearModal}>Clear Sessions</button> <button type="button" className="btn btn-warning" onClick={this.toggleLogoutModal}>Log Out/Switch DB</button> <button type="button" className="btn btn-danger" onClick={this.toggleDBWarnModal}>Clear Database</button> </div> diff --git a/src/components/SearchContainer/Tabs/PrebuiltQueriesDisplay.jsx b/src/components/SearchContainer/Tabs/PrebuiltQueriesDisplay.jsx index 474ec6f00..627b6d9e8 100644 --- a/src/components/SearchContainer/Tabs/PrebuiltQueriesDisplay.jsx +++ b/src/components/SearchContainer/Tabs/PrebuiltQueriesDisplay.jsx @@ -1,17 +1,51 @@ import React, { Component } from 'react'; import PrebuiltQueryNode from './PrebuiltQueryNode' +import { If, Then, Else } from 'react-if'; +const { app } = require('electron').remote var fs = require('fs') +var path = require('path') +var process = require('process') +var exec = require('child_process').exec; export default class PrebuiltQueriesDisplay extends Component { constructor(){ super(); this.state = { - queries: [] + queries: [], + custom: [] } } + getCommandLine() { + switch (process.platform) { + case 'darwin' : return 'open'; + case 'win32' : return 'start'; + case 'win64' : return 'start'; + default : return 'xdg-open'; + } + } + + editCustom(){ + exec(this.getCommandLine() + ' ' + path.join(app.getPath('userData'),'/customqueries.json')); + } + componentWillMount() { + $.ajax({ + url: path.join(app.getPath('userData'),'/customqueries.json'), + type: 'GET', + success: function(response){ + var x = JSON.parse(response) + var y = [] + + $.each(x.queries, function(index, el) { + y.push(el) + }); + + this.setState({custom: y}) + }.bind(this) + }) + $.ajax({ url: 'src/components/SearchContainer/Tabs/PrebuiltQueries.json', type: 'GET', @@ -39,6 +73,23 @@ export default class PrebuiltQueriesDisplay extends Component { return <PrebuiltQueryNode key={a.name} info={a} /> })} </div> + <h3> + Custom Queries + <i className="glyphicon glyphicon-pencil customQueryGlyph" onClick={this.editCustom.bind(this)}></i> + </h3> + <div className="query-box"> + <If condition={ this.state.custom.length == 0}> + <Then><div>No user defined queries.</div></Then> + <Else>{() => + <div> + {this.state.custom.map(function(a){ + return <PrebuiltQueryNode key={a.name} info={a} /> + })} + </div> + } + </Else> + </If> + </div> </div> ); } diff --git a/src/css/styles.css b/src/css/styles.css index d7e22ff7c..ed48b0439 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -32,6 +32,25 @@ body { background-color: lightgray; } +.customQueryGlyph{ + font-size: small; + vertical-align: middle; + top: -1px; + left: 5px; + cursor: pointer; +} + +div.tooltip-inner-custom { + max-width: 250px; +} + +.aboutscroll{ + width: 100%; + overflow: auto; + height: 150px; + white-space: pre-wrap; +} + .queryInput { position: relative; left: 50%; diff --git a/src/index.js b/src/index.js index a9398ec53..0ccf57ebb 100644 --- a/src/index.js +++ b/src/index.js @@ -6,25 +6,35 @@ import AppContainer from './AppContainer'; import Login from './components/Float/Login' import { getStorageData, storageHasKey, storageSetKey } from './js/utils.js'; +const { app } = require('electron').remote +var fs = require('fs') +const path = require('path'); + const ConfigStore = require('configstore'); global.conf = new ConfigStore('bloodhound') var e = require('eventemitter2').EventEmitter2 global.emitter = new e({}) global.renderEmit = new e({}) +global.neo4j = require('neo4j-driver').v1; global.Mustache = require('mustache') -String.prototype.format = function () { - var i = 0, args = arguments; - return this.replace(/{}/g, function () { - return typeof args[i] != 'undefined' ? args[i++] : ''; - }); +String.prototype.format = function() { + var i = 0, + args = arguments; + return this.replace(/{}/g, function() { + return typeof args[i] != 'undefined' ? args[i++] : ''; + }); +}; + +String.prototype.formatAll = function() { + var args = arguments; + return this.replace(/{}/g, args[0]); }; -String.prototype.formatAll = function () { - var args = arguments; - return this.replace(/{}/g, args[0]); +String.prototype.toTitleCase = function() { + return this.replace(/\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); }; Array.prototype.allEdgesSameType = function() { @@ -37,8 +47,8 @@ Array.prototype.allEdgesSameType = function() { return true; }; -if (!Array.prototype.last){ - Array.prototype.last = function(){ +if (!Array.prototype.last) { + Array.prototype.last = function() { return this[this.length - 1]; }; }; @@ -54,16 +64,16 @@ sigma.classes.graph.addMethod('inboundNodes', function(id) { }); global.appStore = { - dagre: false, - startNode: null, - endNode: null, - highlightedEdges: [], - spotlightData: {}, - queryStack: [], - currentTooltip: null, - highResPalette: { - iconScheme: { - 'User': { + dagre: false, + startNode: null, + endNode: null, + highlightedEdges: [], + spotlightData: {}, + queryStack: [], + currentTooltip: null, + highResPalette: { + iconScheme: { + 'User': { font: 'FontAwesome', content: '\uF007', scale: 1.5, @@ -89,89 +99,103 @@ global.appStore = { } }, edgeScheme: { - 'AdminTo': 'tapered', - 'MemberOf': 'tapered', - 'HasSession': 'tapered', - 'TrustedBy' : 'curvedArrow' + 'AdminTo': 'tapered', + 'MemberOf': 'tapered', + 'HasSession': 'tapered', + 'TrustedBy': 'curvedArrow' } - }, - lowResPalette: { - colorScheme: { - 'User' : '#17E625', - 'Computer' : '#E67873', - 'Group' : '#DBE617', - 'Domain' : '#17E6B9' - }, + }, + lowResPalette: { + colorScheme: { + 'User': '#17E625', + 'Computer': '#E67873', + 'Group': '#DBE617', + 'Domain': '#17E6B9' + }, edgeScheme: { - 'AdminTo': 'line', - 'MemberOf': 'line', - 'HasSession': 'line', - 'TrustedBy' : 'curvedArrow' + 'AdminTo': 'line', + 'MemberOf': 'line', + 'HasSession': 'line', + 'TrustedBy': 'curvedArrow' + } + }, + highResStyle: { + nodes: { + label: { + by: 'label' + }, + size: { + by: 'degree', + bins: 5, + min: 10, + max: 20 + }, + icon: { + by: 'type', + scheme: 'iconScheme' + } + }, + edges: { + type: { + by: 'type', + scheme: 'edgeScheme' + } } - }, - highResStyle: { - nodes: { - label: { - by: 'label' - }, - size: { - by: 'degree', - bins: 5, - min: 10, - max: 20 - }, - icon: { - by: 'type', - scheme: 'iconScheme' - } - }, - edges: { - type : { - by : 'type', - scheme: 'edgeScheme' - } - } - }, - lowResStyle: { - nodes: { - label: { - by: 'label' - }, - size: { - by: 'degree', - bins: 10, - min: 10, - max: 20 - }, - color: { - by: 'type', - scheme: 'colorScheme' - } - }, - edges: { - type : { - by : 'type', - scheme: 'edgeScheme' - } - } - } + }, + lowResStyle: { + nodes: { + label: { + by: 'label' + }, + size: { + by: 'degree', + bins: 10, + min: 10, + max: 20 + }, + color: { + by: 'type', + scheme: 'colorScheme' + } + }, + edges: { + type: { + by: 'type', + scheme: 'edgeScheme' + } + } + } } -if (typeof conf.get('performance') === 'undefined'){ - conf.set('performance', { - edge: 5, - sibling: 10, - lowGraphics: false, - nodeLabels: 1 - }) +if (typeof conf.get('performance') === 'undefined') { + conf.set('performance', { + edge: 5, + sibling: 10, + lowGraphics: false, + nodeLabels: 1 + }) } +var custompath = path.join(app.getPath('userData'), 'customqueries.json') + +fs.stat(custompath, function(err, stats) { + if (err) { + fs.writeFile(custompath, "[]") + } +}) + appStore.performance = conf.get('performance') -renderEmit.on('login', function(){ - emitter.removeAllListeners() - ReactDOM.unmountComponentAtNode(document.getElementById('root')) - ReactDOM.render(<AppContainer />, document.getElementById('root')) +renderEmit.on('login', function() { + emitter.removeAllListeners() + ReactDOM.unmountComponentAtNode(document.getElementById('root')) + ReactDOM.render( < AppContainer / > , document.getElementById('root')) +}) + +renderEmit.on('logout', function() { + emitter.removeAllListeners() + ReactDOM.unmountComponentAtNode(document.getElementById('root')) + ReactDOM.render( < Login / > , document.getElementById('root')) }) -ReactDOM.render(<Login />, document.getElementById('root')) \ No newline at end of file +ReactDOM.render( < Login / > , document.getElementById('root')) \ No newline at end of file diff --git a/src/js/utils.js b/src/js/utils.js index bc839217e..53252ebbc 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -1,149 +1,206 @@ -export function generateUniqueId(sigmaInstance, isNode){ - var i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10; - if (isNode){ - while (typeof sigmaInstance.graph.nodes(i) !== 'undefined'){ - i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10; - } - }else{ - while (typeof sigmaInstance.graph.edges(i) !== 'undefined'){ - i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10; - } - } - - return i +export function generateUniqueId(sigmaInstance, isNode) { + var i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10; + if (isNode) { + while (typeof sigmaInstance.graph.nodes(i) !== 'undefined') { + i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10; + } + } else { + while (typeof sigmaInstance.graph.edges(i) !== 'undefined') { + i = Math.floor(Math.random() * (100000 - 10 + 1)) + 10; + } + } + + return i } //Recursive function to highlight paths to start/end nodes -export function findGraphPath(sigmaInstance, reverse, nodeid){ - var target = reverse ? appStore.startNode : appStore.endNode - //This is our stop condition for recursing - if (nodeid !== target.id){ - var edges = sigmaInstance.graph.adjacentEdges(nodeid) - var nodes = reverse ? sigmaInstance.graph.inboundNodes(nodeid) : sigmaInstance.graph.outboundNodes(nodeid) - //Loop over the nodes near us and the edges connecting to those nodes - $.each(nodes, function(index, node){ - $.each(edges, function(index, edge){ - var check = reverse ? edge.source : edge.target - //If an edge is pointing in the right direction, set its color - //Push the edge into our store and then - node = parseInt(node) - if (check === node){ - edge.color = reverse ? 'blue' : 'red'; - appStore.highlightedEdges.push(edge); - findGraphPath(sigmaInstance, reverse, node); - } - }) - }) - }else{ - return - } +export function findGraphPath(sigmaInstance, reverse, nodeid) { + var target = reverse ? appStore.startNode : appStore.endNode + //This is our stop condition for recursing + if (nodeid !== target.id) { + var edges = sigmaInstance.graph.adjacentEdges(nodeid) + var nodes = reverse ? sigmaInstance.graph.inboundNodes(nodeid) : sigmaInstance.graph.outboundNodes(nodeid) + //Loop over the nodes near us and the edges connecting to those nodes + $.each(nodes, function(index, node) { + $.each(edges, function(index, edge) { + var check = reverse ? edge.source : edge.target + //If an edge is pointing in the right direction, set its color + //Push the edge into our store and then + node = parseInt(node) + if (check === node) { + edge.color = reverse ? 'blue' : 'red'; + appStore.highlightedEdges.push(edge); + findGraphPath(sigmaInstance, reverse, node); + } + }) + }) + } else { + return + } +} + +export function clearSessions(){ + emitter.emit('openClearingModal'); + deleteSessions(); +} + +function deleteSessions(){ + var session = driver.session() + session.run("MATCH ()-[r:HasSession]-() WITH r LIMIT 100000 DELETE r RETURN count(r)") + .then(function(results) { + session.close() + emitter.emit("refreshDBData") + var count = results.records[0]._fields[0].low + if (count === 0) { + emitter.emit('hideDBClearModal') + } else { + deleteSessions(); + } + }) +} + +export function clearDatabase() { + emitter.emit('openClearingModal'); + deleteEdges() } -export function clearDatabase(){ - emitter.emit('openClearingModal'); - deleteEdges() +function deleteEdges() { + var session = driver.session() + session.run("MATCH ()-[r]-() WITH r LIMIT 100000 DELETE r RETURN count(r)") + .then(function(results) { + emitter.emit("refreshDBData"); + session.close() + var count = results.records[0]._fields[0].low + if (count === 0) { + deleteNodes() + } else { + deleteEdges() + } + }) } -function deleteEdges(){ - var session = driver.session() - session.run("MATCH ()-[r]-() WITH r LIMIT 100000 DELETE r RETURN count(r)") - .then(function(results){ - session.close() - var count = results.records[0]._fields[0].low - if (count === 0){ - deleteNodes() - }else{ - deleteEdges() - } - }) +function deleteNodes() { + var session = driver.session() + session.run("MATCH (n) WITH n LIMIT 100000 DELETE n RETURN count(n)") + .then(function(results) { + emitter.emit("refreshDBData") + session.close() + var count = results.records[0]._fields[0].low + if (count === 0) { + emitter.emit('hideDBClearModal') + } else { + deleteNodes() + } + }) } -function deleteNodes(){ - var session = driver.session() - session.run("MATCH (n) WITH n LIMIT 100000 DELETE n RETURN count(n)") - .then(function(results){ - session.close() - var count = results.records[0]._fields[0].low - if (count === 0){ - emitter.emit('hideDBClearModal') - }else{ - deleteNodes() - } - }) +export function buildGroupMembershipProps(rows) { + var users = [] + var groups = [] + var computers = [] + $.each(rows, function(index, row) { + switch (row.AccountType) { + case 'user': + users.push({ account: row.AccountName.toUpperCase(), group: row.GroupName.toUpperCase() }) + break + case 'computer': + computers.push({ account: row.AccountName.toUpperCase(), group: row.GroupName.toUpperCase() }) + break + case 'group': + groups.push({ account: row.AccountName.toUpperCase(), group: row.GroupName.toUpperCase() }) + break + } + }) + + return { users: users, groups: groups, computers: computers } } -export function buildGroupMembershipProps(rows){ - var users = [] - var groups = [] - var computers = [] - $.each(rows, function(index, row){ - switch (row.AccountType){ - case 'user': - users.push({account:row.AccountName.toUpperCase(), group: row.GroupName.toUpperCase()}) - break - case 'computer': - computers.push({account:row.AccountName.toUpperCase(), group: row.GroupName.toUpperCase()}) - break - case 'group': - groups.push({account:row.AccountName.toUpperCase(), group: row.GroupName.toUpperCase()}) - break - } - }) - - return {users: users, groups: groups, computers: computers} +export function buildLocalAdminProps(rows) { + var users = [] + var groups = [] + var computers = [] + $.each(rows, function(index, row) { + if (row.AccountName.startsWith('@')) { + return + } + switch (row.AccountType) { + case 'user': + users.push({ account: row.AccountName.toUpperCase(), computer: row.ComputerName.toUpperCase() }) + break; + case 'group': + groups.push({ account: row.AccountName.toUpperCase(), computer: row.ComputerName.toUpperCase() }) + break; + case 'computer': + computers.push({ account: row.AccountName.toUpperCase(), computer: row.ComputerName.toUpperCase() }) + break + } + }) + return { users: users, groups: groups, computers: computers } } -export function buildLocalAdminProps(rows){ - var users = [] - var groups = [] - var computers = [] - $.each(rows, function(index, row){ - if (row.AccountName.startsWith('@')){ - return - } - switch(row.AccountType){ - case 'user': - users.push({account:row.AccountName.toUpperCase(), computer: row.ComputerName.toUpperCase()}) - break; - case 'group': - groups.push({account:row.AccountName.toUpperCase(), computer: row.ComputerName.toUpperCase()}) - break; - case 'computer': - computers.push({account:row.AccountName.toUpperCase(), computer: row.ComputerName.toUpperCase()}) - break - } - }) - return {users: users, groups: groups, computers: computers} +export function buildSessionProps(rows) { + var sessions = [] + $.each(rows, function(index, row) { + if (row.UserName === 'ANONYMOUS LOGON@UNKNOWN' || row.UserName === '') { + return + } + sessions.push({ account: row.UserName.toUpperCase(), computer: row.ComputerName.toUpperCase(), weight: row.Weight }) + }) + + return sessions } -export function buildSessionProps(rows){ - var sessions = [] - $.each(rows, function(index, row){ - if (row.UserName === 'ANONYMOUS LOGON@UNKNOWN' || row.UserName === ''){ - return - } - sessions.push({account: row.UserName.toUpperCase(), computer: row.ComputerName.toUpperCase(), weight: row.Weight}) - }) +export function buildDomainProps(rows) { + var domains = [] + $.each(rows, function(index, row) { + switch (row.TrustDirection) { + case 'Inbound': + domains.push({ domain1: row.TargetDomain.toUpperCase(), domain2: row.SourceDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive }) + break; + case 'Outbound': + domains.push({ domain1: row.SourceDomain.toUpperCase(), domain2: row.TargetDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive }) + break; + case 'Bidirectional': + domains.push({ domain1: row.TargetDomain.toUpperCase(), domain2: row.SourceDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive }) + domains.push({ domain1: row.SourceDomain.toUpperCase(), domain2: row.TargetDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive }) + break + } + }) - return sessions + return domains } -export function buildDomainProps(rows){ - var domains = [] - $.each(rows, function(index, row){ - switch(row.TrustDirection){ - case 'Inbound': - domains.push({domain1: row.TargetDomain.toUpperCase(), domain2: row.SourceDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive}) - break; - case 'Outbound': - domains.push({domain1: row.SourceDomain.toUpperCase(), domain2: row.TargetDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive}) - break; - case 'Bidirectional': - domains.push({domain1: row.TargetDomain.toUpperCase(), domain2: row.SourceDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive}) - domains.push({domain1: row.SourceDomain.toUpperCase(), domain2: row.TargetDomain.toUpperCase(), trusttype: row.TrustType, transitive: row.Transitive}) - break - } - }) - - return domains +export function buildACLProps(rows) { + var datadict = {} + + $.each(rows, function(index, row) { + var b = row.ObjectName.toUpperCase() + var a = row.PrincipalName.toUpperCase() + var btype = row.ObjectType.toTitleCase() + var atype = row.PrincipalType.toTitleCase() + var rel = row.ActiveDirectoryRights + + var hash = (atype + rel + btype).toUpperCase() + if (btype === 'Computer') { + return + } + + if (rel.includes(' ')) { + rel = 'WriteDACL' + } + + if (datadict[hash]) { + datadict[hash].props.push({ + account: a, + principal: b + }) + } else { + datadict[hash] = { + statement: 'UNWIND {props} AS prop MERGE (a:{} {name:prop.account}) WITH a,prop MERGE (b:{} {name: prop.principal}) WITH a,b,prop MERGE (a)-[r:{}]->(b)'.format(atype, btype, rel), + props: [{ account: a, principal: b }] + } + } + }) + + return datadict } \ No newline at end of file