Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADCS/PKI - Merge ly4k/BloodHound with main branch #684

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
7 changes: 6 additions & 1 deletion src/AppContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ const fullEdgeList = [
'AddKeyCredentialLink',
'DumpSMSAPassword',
'DCSync',
'SyncLAPSPassword'
'SyncLAPSPassword',
'Enroll',
'AutoEnroll',
'ManageCa',
'ManageCertificates',
'EnabledBy'
];

export default class AppContainer extends Component {
Expand Down
19 changes: 16 additions & 3 deletions src/components/RawQuery.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ const RawQuery = () => {
const onKeyDown = (e) => {
let key = e.keyCode ? e.keyCode : e.which;

if (key === 13) {
if ((key == 10 || key == 13) && e.ctrlKey) {
emitter.emit('query', query);
}
};

const onChange = (e) => {
e.target.style.height = 'inherit';
e.target.style.height = `${e.target.scrollHeight}px`;
setQueryFromEvent(e.target.value);
};

Expand Down Expand Up @@ -59,15 +61,26 @@ const RawQuery = () => {
transition={{ duration: 0.25 }}
animate={open ? 'open' : 'closed'}
>
<input
{/* <input
type='text'
onChange={onChange}
value={query}
onKeyDown={onKeyDown}
className={clsx(styles.input, 'form-control')}
autoComplete='off'
placeholder='Enter a cypher query. Your query must return nodes or paths.'
/>
/> */}
<textarea
type='text'
onChange={onChange}
value={query}
onKeyDown={onKeyDown}
className={clsx(styles.input, 'form-control')}
autoComplete='off'
rows='1'
placeholder='Enter a cypher query. Your query must return nodes or paths. Use Ctrl+Enter to submit'>

</textarea>
</motion.div>
</div>
);
Expand Down
15 changes: 15 additions & 0 deletions src/components/SearchContainer/EdgeFilter/EdgeFilter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@ const EdgeFilter = ({ open }) => {
<EdgeFilterCheck name='SQLAdmin' />
<EdgeFilterCheck name='HasSIDHistory' />
<EdgeFilterCheck name='DumpSMSAPassword' />
<EdgeFilterSection
title='PKI'
sectionName='pki'
edges={[
'Enroll',
'AutoEnroll',
'ManageCa',
'ManageCertificates',
'EnabledBy',
]}
/>
<EdgeFilterCheck name='Enroll' />
<EdgeFilterCheck name='AutoEnroll' />
<EdgeFilterCheck name='ManageCa' />
<EdgeFilterCheck name='ManageCertificates' />
</div>
<div>
<EdgeFilterSection
Expand Down
20 changes: 17 additions & 3 deletions src/components/SearchContainer/SearchRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ const SearchRow = ({ item, search }) => {

switch (type) {
case 'Group':
icon.className = 'fa fa-users';
if (item.hasOwnProperty("bl-icon")){
icon.className = 'fa '+ item["bl-icon"];
}else{
icon.className = 'fa fa-users';
}
break;
case 'User':
icon.className = 'fa fa-user';
Expand All @@ -43,6 +47,12 @@ const SearchRow = ({ item, search }) => {
case 'OU':
icon.className = 'fa fa-sitemap';
break;
case 'CA':
icon.className = 'fa fa-university';
break;
case 'CertificateTemplate':
icon.className = 'fa fa-id-card';
break;
case 'Container':
icon.className = 'fa fa-box'
break
Expand Down Expand Up @@ -104,8 +114,12 @@ const SearchRow = ({ item, search }) => {
icon.className = 'fa fa-window-restore'
break
default:
icon.className = 'fa fa-question';
type = 'Base';
if (item.hasOwnProperty("bl-icon")){
icon.className = 'fa '+ item["bl-icon"];
}else{
icon.className = 'fa fa-question';
type = 'Base';
}
break;
}

Expand Down
28 changes: 28 additions & 0 deletions src/components/SearchContainer/TabContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import ComputerNodeData from './Tabs/ComputerNodeData';
import DomainNodeData from './Tabs/DomainNodeData';
import GpoNodeData from './Tabs/GPONodeData';
import OuNodeData from './Tabs/OUNodeData';
import CaNodeData from './Tabs/CANodeData';
import TemplateNodeData from './Tabs/TemplateNodeData';
import AZGroupNodeData from './Tabs/AZGroupNodeData';
import AZUserNodeData from './Tabs/AZUserNodeData';
import AZContainerRegistryNodeData from './Tabs/AZContainerRegistryNodeData';
Expand Down Expand Up @@ -48,6 +50,8 @@ class TabContainer extends Component {
domainVisible: false,
gpoVisible: false,
ouVisible: false,
caVisible: false,
templateVisible: false,
containerVisible: false,
azGroupVisible: false,
azUserVisible: false,
Expand Down Expand Up @@ -92,6 +96,10 @@ class TabContainer extends Component {
this._domainNodeClicked();
} else if (type === 'OU') {
this._ouNodeClicked();
} else if (type === 'CA') {
this._caNodeClicked();
} else if (type === 'CertificateTemplate') {
this._templateNodeClicked();
} else if (type === 'GPO') {
this._gpoNodeClicked();
} else if (type === 'AZGroup') {
Expand Down Expand Up @@ -225,6 +233,22 @@ class TabContainer extends Component {
});
}

_caNodeClicked() {
this.clearVisible()
this.setState({
caVisible: true,
selected: 2
});
}

_templateNodeClicked() {
this.clearVisible()
this.setState({
templateVisible: true,
selected: 2
});
}

_azGroupNodeClicked() {
this.clearVisible()
this.setState({
Expand Down Expand Up @@ -405,6 +429,8 @@ class TabContainer extends Component {
!this.state.domainVisible &&
!this.state.gpoVisible &&
!this.state.ouVisible &&
!this.state.caVisible &&
!this.state.templateVisible &&
!this.state.azGroupVisible &&
!this.state.azUserVisible &&
!this.state.azContainerRegistryVisible &&
Expand Down Expand Up @@ -436,6 +462,8 @@ class TabContainer extends Component {
<DomainNodeData visible={this.state.domainVisible} />
<GpoNodeData visible={this.state.gpoVisible} />
<OuNodeData visible={this.state.ouVisible} />
<CaNodeData visible={this.state.caVisible} />
<TemplateNodeData visible={this.state.templateVisible} />
<ContainerNodeData visible={this.state.containerVisible} />
<AZGroupNodeData visible={this.state.azGroupVisible} />
<AZUserNodeData visible={this.state.azUserVisible} />
Expand Down
171 changes: 171 additions & 0 deletions src/components/SearchContainer/Tabs/CANodeData.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/******************************************************************************************
* The credit goes to https://github.com/ly4k/BloodHound. Thank you for the excellent work!
*/
import clsx from 'clsx';
import React, { useContext, useEffect, useState } from 'react';
import { Table } from 'react-bootstrap';
import { AppContext } from '../../../AppContext';
import CollapsibleSection from './Components/CollapsibleSection';
import ExtraNodeProps from './Components/ExtraNodeProps';
import MappedNodeProps from './Components/MappedNodeProps';
import NodeCypherLink from './Components/NodeCypherLink';
import NodePlayCypherLink from './Components/NodePlayCypherLink';
import NodeCypherNoNumberLink from './Components/NodeCypherNoNumberLink';
import styles from './NodeData.module.css';

const CANodeData = () => {
const [visible, setVisible] = useState(false);
const [objectid, setObjectid] = useState(null);
const [label, setLabel] = useState(null);
const [domain, setDomain] = useState(null);
const [nodeProps, setNodeProps] = useState({});
const [blocksInheritance, setBlocksInheritance] = useState(false);
const context = useContext(AppContext);

useEffect(() => {
emitter.on('nodeClicked', nodeClickEvent);

return () => {
emitter.removeListener('nodeClicked', nodeClickEvent);
};
}, []);

const nodeClickEvent = (type, id, blocksinheritance, domain) => {
if (type === 'CA') {
setVisible(true);
setObjectid(id);
setDomain(domain);
setBlocksInheritance(blocksinheritance);
let session = driver.session();
session
.run(`MATCH (n:CA {objectid: $objectid}) RETURN n AS node`, {
objectid: id,
})
.then((r) => {
let props = r.records[0].get('node').properties;
setNodeProps(props);
setLabel(props.name);
session.close();
});
} else {
setObjectid(null);
setVisible(false);
}
};

const displayMap = {
objectid: 'Object ID',
'CA Name': 'CA Name',
};

return objectid === null ? (
<div></div>
) : (
<div
className={clsx(
!visible && 'displaynone',
context.darkMode ? styles.dark : styles.light
)}
>
<div className={clsx(styles.dl)}>
<h5>{label || objectid}</h5>

<CollapsibleSection header='OVERVIEW'>
<div className={styles.itemlist}>
<Table>
<thead></thead>
<tbody className='searchable'>
<NodeCypherNoNumberLink
query='MATCH p = (c:CA {objectid: $objectid})<-[r:EnabledBy]-(t:CertificateTemplate) RETURN p'
target={objectid}
property='See Enabled Templates'
/>
</tbody>
</Table>
</div>
</CollapsibleSection>

<hr></hr>

<MappedNodeProps
displayMap={displayMap}
properties={nodeProps}
label={label}
/>

<hr></hr>

<ExtraNodeProps
displayMap={displayMap}
properties={nodeProps}
label={label}
/>

<hr></hr>

<CollapsibleSection header={'INBOUND CONTROL RIGHTS'}>
<div className={styles.itemlist}>
<Table>
<thead></thead>
<tbody className='searchable'>
<NodeCypherLink
property='Explicit Object Controllers'
target={objectid}
baseQuery={
'MATCH p=(n)-[r:ManageCa|ManageCertificates|Auditor|Operator|Owns]->(u1:CA {objectid: $objectid}) WHERE r.isacl=true'
}
end={label}
distinct
/>
<NodeCypherLink
property='Unrolled Object Controllers'
target={objectid}
baseQuery={
'MATCH p=(n)-[r:MemberOf*1..]->(g:Group)-[r1:ManageCa|ManageCertificates|Auditor|Operator|Owns]->(u:CA {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid'
}
end={label}
distinct
/>
</tbody>
</Table>
</div>
</CollapsibleSection>

<hr></hr>

<CollapsibleSection header={'INBOUND ACCESS RIGHTS'}>
<div className={styles.itemlist}>
<Table>
<thead></thead>
<tbody className='searchable'>
<NodeCypherLink
property='First Degree Access Rights'
target={objectid}
baseQuery={
'MATCH p=(n)-[r:Read|Enroll]->(u1:CA {objectid: $objectid}) WHERE r.isacl=true'
}
end={label}
distinct
/>
<NodeCypherLink
property='Group Delegated Access Rights'
target={objectid}
baseQuery={
'MATCH p=(n)-[r:MemberOf*1..]->(g:Group)-[r1:Read|Enroll]->(u:CA {objectid: $objectid}) WITH LENGTH(p) as pathLength, p, n WHERE NONE (x in NODES(p)[1..(pathLength-1)] WHERE x.objectid = u.objectid) AND NOT n.objectid = u.objectid'
}
end={label}
distinct
/>
</tbody>
</Table>
</div>
</CollapsibleSection>

<hr></hr>
</div>
</div>
);
};

CANodeData.propTypes = {};
export default CANodeData;
14 changes: 14 additions & 0 deletions src/components/SearchContainer/Tabs/DatabaseDataDisplay.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ const DatabaseDataDisplay = () => {
index={index}
label={'Computers'}
/>
<DatabaseDataLabel
query={
'MATCH (n:CertificateTemplate) RETURN count(n) AS count'
}
index={index}
label={'Certificate Templates'}
/>
<DatabaseDataLabel
query={
'MATCH (n:CA) RETURN count(n) AS count'
}
index={index}
label={'Certificate Authorities'}
/>
<DatabaseDataLabel
query={'MATCH (n:OU) RETURN count(n) AS count'}
index={index}
Expand Down
Loading