Added ScoutSuite UI code

This commit is contained in:
VakarisZ 2020-09-18 10:26:25 +03:00
parent 4440027699
commit c66cb11e79
18 changed files with 519 additions and 45 deletions

View File

@ -1,9 +1,10 @@
from typing import List
from mongoengine import DateTimeField, Document, StringField, EmbeddedDocumentListField
from mongoengine import Document, EmbeddedDocumentListField
from monkey_island.cc.models.zero_trust.event import Event
class MonkeyFindingDetails(Document):
"""
This model represents additional information about monkey finding:

View File

@ -60,7 +60,7 @@ class ReportPageComponent extends AuthComponent {
}
getZeroTrustReportFromServer = async () => {
let ztReport = {findings: {}, principles: {}, pillars: {}};
let ztReport = {findings: {}, principles: {}, pillars: {}, scoutsuite_data: {}};
await this.authFetch('/api/report/zero_trust/findings')
.then(res => res.json())
.then(res => {
@ -76,6 +76,11 @@ class ReportPageComponent extends AuthComponent {
.then(res => {
ztReport.pillars = res;
});
await this.authFetch('/api/report/zero_trust/scoutsuite')
.then(res => res.json())
.then(res => {
ztReport.scoutsuite_data = res;
});
return ztReport
};

View File

@ -16,7 +16,7 @@ class ZeroTrustReportPageComponent extends AuthComponent {
componentDidUpdate(prevProps) {
if (this.props.report !== prevProps.report) {
this.setState(this.props.report)
this.setState(this.props.report)
}
}
@ -29,7 +29,9 @@ class ZeroTrustReportPageComponent extends AuthComponent {
<SummarySection allMonkeysAreDead={this.state.allMonkeysAreDead} pillars={this.state.pillars}/>
<PrinciplesSection principles={this.state.principles}
pillarsToStatuses={this.state.pillars.pillarsToStatuses}/>
<FindingsSection pillarsToStatuses={this.state.pillars.pillarsToStatuses} findings={this.state.findings}/>
<FindingsSection pillarsToStatuses={this.state.pillars.pillarsToStatuses}
findings={this.state.findings}
scoutsuite_data={this.state.scoutsuite_data}/>
</div>;
}
@ -57,7 +59,8 @@ class ZeroTrustReportPageComponent extends AuthComponent {
stillLoadingDataFromServer() {
return typeof this.state.findings === 'undefined'
|| typeof this.state.pillars === 'undefined'
|| typeof this.state.principles === 'undefined';
|| typeof this.state.principles === 'undefined'
|| typeof this.state.scoutsuite_data === 'undefined';
}

View File

@ -0,0 +1,6 @@
const RULE_LEVELS = {
LEVEL_WARNING: 'warning',
LEVEL_DANGER: 'danger'
}
export default RULE_LEVELS

View File

@ -0,0 +1,8 @@
const STATUSES = {
STATUS_UNEXECUTED: 'Unexecuted',
STATUS_PASSED: 'Passed',
STATUS_VERIFY: 'Verify',
STATUS_FAILED: 'Failed'
}
export default STATUSES

View File

@ -32,9 +32,15 @@ class FindingsSection extends Component {
insight as to what exactly happened during this test.
</p>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.failed]} status={ZeroTrustStatuses.failed}/>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.verify]} status={ZeroTrustStatuses.verify}/>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.passed]} status={ZeroTrustStatuses.passed}/>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.failed]}
scoutsuite_data={this.props.scoutsuite_data}
status={ZeroTrustStatuses.failed}/>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.verify]}
scoutsuite_data={this.props.scoutsuite_data}
status={ZeroTrustStatuses.verify}/>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.passed]}
scoutsuite_data={this.props.scoutsuite_data}
status={ZeroTrustStatuses.passed}/>
</div>
);
}

View File

@ -4,52 +4,59 @@ import PaginatedTable from '../common/PaginatedTable';
import * as PropTypes from 'prop-types';
import PillarLabel from './PillarLabel';
import EventsButton from './EventsButton';
import ScoutSuiteRuleButton from './scoutsuite/ScoutSuiteRuleButton';
const EVENTS_COLUMN_MAX_WIDTH = 170;
const EVENTS_COLUMN_MAX_WIDTH = 250;
const PILLARS_COLUMN_MAX_WIDTH = 200;
const columns = [
{
columns: [
{
Header: 'Finding', accessor: 'test',
style: {'whiteSpace': 'unset'} // This enables word wrap
},
{
Header: 'Events', id: 'events',
accessor: x => {
return <EventsButton finding_id={x.finding_id}
latest_events={x.latest_events}
oldest_events={x.oldest_events}
event_count={x.event_count}
exportFilename={'Events_' + x.test_key} />;
},
maxWidth: EVENTS_COLUMN_MAX_WIDTH
},
{
Header: 'Pillars', id: 'pillars',
accessor: x => {
const pillars = x.pillars;
const pillarLabels = pillars.map((pillar) =>
<PillarLabel key={pillar.name} pillar={pillar.name} status={pillar.status}/>
);
return <div style={{textAlign: 'center'}}>{pillarLabels}</div>;
},
maxWidth: PILLARS_COLUMN_MAX_WIDTH,
style: {'whiteSpace': 'unset'}
}
]
}
];
export class FindingsTable extends Component {
columns = [
{
columns: [
{
Header: 'Finding', accessor: 'test',
style: {'whiteSpace': 'unset'} // This enables word wrap
},
{
Header: 'Details', id: 'details',
accessor: x => {
if (x.type === 'scoutsuite_finding') {
return <ScoutSuiteRuleButton scoutsuite_rules={x.details.scoutsuite_rules}
scoutsuite_data={this.props.scoutsuite_data}/>;
} else if (x.type === 'monkey_finding') {
return <EventsButton finding_id={x.finding_id}
latest_events={x.details.latest_events}
oldest_events={x.details.oldest_events}
event_count={x.details.event_count}
exportFilename={'Events_' + x.test_key}/>;
}
},
maxWidth: EVENTS_COLUMN_MAX_WIDTH
},
{
Header: 'Pillars', id: 'pillars',
accessor: x => {
const pillars = x.pillars;
const pillarLabels = pillars.map((pillar) =>
<PillarLabel key={pillar.name} pillar={pillar.name} status={pillar.status}/>
);
return <div style={{textAlign: 'center'}}>{pillarLabels}</div>;
},
maxWidth: PILLARS_COLUMN_MAX_WIDTH,
style: {'whiteSpace': 'unset'}
}
]
}
];
render() {
return <Fragment>
<h3>{<span style={{display: 'inline-block'}}><StatusLabel status={this.props.status} showText={true}/>
</span>} tests' findings</h3>
<PaginatedTable data={this.props.data} pageSize={10} columns={columns}/>
<PaginatedTable data={this.props.data} pageSize={10} columns={this.columns}/>
</Fragment>
}
}

View File

@ -0,0 +1,75 @@
import React, {useState} from 'react';
import * as PropTypes from 'prop-types';
import '../../../../styles/components/scoutsuite/RuleDisplay.scss'
import classNames from 'classnames';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown';
import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp';
import ScoutSuiteDataParser from './ScoutSuiteDataParser';
import Collapse from '@kunukn/react-collapse';
import {faArrowRight} from '@fortawesome/free-solid-svg-icons';
export default function ResourceDropdown(props) {
const [isCollapseOpen, setIsCollapseOpen] = useState(false);
function getResourceDropdown() {
return (
<div key={props.resource_path} className={classNames('collapse-item',
'resource-collapse', {'item--active': isCollapseOpen})}>
<button className={'btn-collapse'}
onClick={() => setIsCollapseOpen(!isCollapseOpen)}>
<span>
{props.resource_path}
</span>
<span>
<FontAwesomeIcon icon={isCollapseOpen ? faChevronDown : faChevronUp}/>
</span>
</button>
<Collapse
className='collapse-comp'
isOpen={isCollapseOpen}
render={getResourceDropdownContents}/>
</div>
);
}
function replacePathDotsWithArrows(resourcePath) {
let path_vars = resourcePath.split('.')
let display_path = []
for(let i = 0; i < path_vars.length; i++){
display_path.push(path_vars[i])
if( i !== path_vars.length - 1) {
display_path.push(<FontAwesomeIcon icon={faArrowRight} />)
}
}
return display_path;
}
function prettyPrintJson(data) {
return JSON.stringify(data, null, 4);
}
function getResourceDropdownContents() {
let parser = new ScoutSuiteDataParser(props.scoutsuite_data.data.services);
return (
<div className={'resource-display'}>
<div>
<p className={'resource-path-title'}>Path:</p>
<p className={'resource-path-contents'}>{replacePathDotsWithArrows(props.resource_path)}</p>
</div>
<div>
<p className={'resource-value-title'}>Value:</p>
<pre className={'resource-value-json'}>{prettyPrintJson(parser.getValueAt(props.resource_path))}</pre>
</div>
</div>
);
}
return getResourceDropdown();
}
ResourceDropdown.propTypes = {
resource_path: PropTypes.object,
scoutsuite_data: PropTypes.object
};

View File

@ -0,0 +1,56 @@
import React from 'react';
import * as PropTypes from 'prop-types';
import '../../../../styles/components/scoutsuite/RuleDisplay.scss'
import ResourceDropdown from './ResourceDropdown';
export default function RuleDisplay(props) {
return (
<div className={'scoutsuite-rule-display'}>
<div className={'description'}>
<h3>{props.rule.description}({props.rule.service})</h3>
</div>
<div className={'rationale'}>
<p>{props.rule.rationale}</p>
</div>
<div className={'checked-resources'}>
<p className={'checked-resources-title'}>Resources checked: </p>
<p>{props.rule.checked_items}</p>
</div>
<div className={'flagged-resources'}>
<p className={'checked-resources-title'}>Resources flagged: </p>
<p>{props.rule.flagged_items}</p>
</div>
{props.rule.references.length !== 0 ? getReferences() : ''}
{props.rule.items.length !== 0 ? getResources() : ''}
</div>);
function getReferences() {
let references = []
props.rule.references.forEach(reference => {
references.push(<a href={reference} className={'reference-link'} target={'_blank'}>{reference}</a>)
})
return (
<div className={'reference-list'}>
<p className={'reference-list-title'}>References:</p>
{references}
</div>)
}
function getResources() {
let resources = []
props.rule.items.forEach(item => {
resources.push(<ResourceDropdown resource_path={item} scoutsuite_data={props.scoutsuite_data}/>)
})
return (
<div className={'reference-list'}>
<p className={'reference-list-title'}>Resources:</p>
{resources}
</div>)
}
}
RuleDisplay.propTypes = {
rule: PropTypes.object,
scoutsuite_data: PropTypes.object
};

View File

@ -0,0 +1,58 @@
export default class ScoutSuiteDataParser {
constructor(runResults) {
this.runResults = runResults
}
getValueAt(path) {
return this.getValueAtRecursive(path, this.runResults)
}
getValueAtRecursive(path, source) {
let value = source;
let current_path = path;
let key;
// iterate over each path elements
while (current_path) {
// check if there are more elements to the path
if (current_path.indexOf('.') != -1) {
key = current_path.substr(0, current_path.indexOf('.'));
}
// last element
else {
key = current_path;
}
try {
// path containing an ".id"
if (key == 'id') {
let v = [];
let w;
for (let k in value) {
// process recursively
w = this.getValueAtRecursive(k + current_path.substr(current_path.indexOf('.'), current_path.length), value);
v = v.concat(
Object.values(w) // get values from array, otherwise it will be an array of key/values
);
}
return v;
}
// simple path, just return element in value
else {
value = value[key];
}
} catch (err) {
console.log('Error: ' + err)
}
// check if there are more elements to process
if (current_path.indexOf('.') != -1) {
current_path = current_path.substr(current_path.indexOf('.') + 1, current_path.length);
}
// otherwise we're done
else {
current_path = false;
}
}
return value;
}
}

View File

@ -0,0 +1,45 @@
import React, {Component} from 'react';
import {Badge, Button} from 'react-bootstrap';
import * as PropTypes from 'prop-types';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faList} from '@fortawesome/free-solid-svg-icons/faList';
import ScoutSuiteRuleModal from './ScoutSuiteRuleModal';
export default class ScoutSuiteRuleButton extends Component {
constructor(props) {
super(props);
this.state = {
isModalOpen: false
}
}
toggleModal = () => {
this.setState({isModalOpen: !this.state.isModalOpen});
};
render() {
return (
<>
<ScoutSuiteRuleModal scoutsuite_rules={this.props.scoutsuite_rules}
scoutsuite_data={this.props.scoutsuite_data}
isModalOpen={this.state.isModalOpen}
hideCallback={this.toggleModal} />
<div className="text-center" style={{'display': 'grid'}}>
<Button variant={'monkey-info'} size={'lg'} onClick={this.toggleModal}>
<FontAwesomeIcon icon={faList}/> ScoutSuite rules {this.createRuleCountBadge()}
</Button>
</div>
</>);
}
createRuleCountBadge() {
const ruleCount = this.props.scoutsuite_rules.length > 9 ? '9+' : this.props.scoutsuite_rules.length;
return <Badge variant={'monkey-info-light'}>{ruleCount}</Badge>;
}
}
ScoutSuiteRuleButton.propTypes = {
scoutsuite_rules: PropTypes.array,
scoutsuite_data: PropTypes.object
};

View File

@ -0,0 +1,58 @@
import React, {useState} from 'react';
import {Modal} from 'react-bootstrap';
import * as PropTypes from 'prop-types';
import Pluralize from 'pluralize';
import ScoutSuiteSingleRuleDropdown from './ScoutSuiteSingleRuleDropdown';
import '../../../../styles/components/scoutsuite/RuleModal.scss';
export default function ScoutSuiteRuleModal(props) {
const [openRuleId, setOpenRuleId] = useState(null)
function toggleRuleDropdown(ruleId) {
if (openRuleId === ruleId) {
setOpenRuleId(null);
} else {
setOpenRuleId(ruleId);
}
}
function renderRuleDropdowns() {
let dropdowns = [];
props.scoutsuite_rules.forEach(rule => {
let dropdown = (<ScoutSuiteSingleRuleDropdown isCollapseOpen={openRuleId === rule.description}
toggleCallback={() => toggleRuleDropdown(rule.description)}
rule={rule}
scoutsuite_data={props.scoutsuite_data}/>)
dropdowns.push(dropdown)
});
return dropdowns;
}
return (
<div>
<Modal show={props.isModalOpen} onHide={() => props.hideCallback()} className={'scoutsuite-rule-modal'}>
<Modal.Body>
<h3>
<div className="text-center">ScoutSuite rules</div>
</h3>
<hr/>
<p>
There {Pluralize('is', props.scoutsuite_rules.length)} {
<div className={'badge badge-primary'}>{props.scoutsuite_rules.length}</div>
} ScoutSuite {Pluralize('rule', props.scoutsuite_rules.length)} associated with finding.
</p>
{renderRuleDropdowns()}
</Modal.Body>
</Modal>
</div>
);
}
ScoutSuiteRuleModal.propTypes = {
isModalOpen: PropTypes.bool,
scoutsuite_rules: PropTypes.array,
scoutsuite_data: PropTypes.object,
hideCallback: PropTypes.func
};

View File

@ -0,0 +1,90 @@
import React from 'react';
import Collapse from '@kunukn/react-collapse';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp'
import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'
import classNames from 'classnames';
import * as PropTypes from 'prop-types';
import RULE_LEVELS from '../../common/consts/ScoutSuiteConsts/RuleLevels';
import STATUSES from '../../common/consts/StatusConsts';
import {faCheckCircle, faCircle, faExclamationCircle} from '@fortawesome/free-solid-svg-icons';
import RuleDisplay from './RuleDisplay';
export default function ScoutSuiteSingleRuleDropdown(props) {
function getRuleCollapse() {
return (
<div key={props.rule.description} className={classNames('collapse-item',
'rule-collapse', {'item--active': props.isCollapseOpen})}>
<button className={classNames('btn-collapse', getDropdownClass())}
onClick={props.toggleCallback}>
<span>
<FontAwesomeIcon icon={getRuleIcon()} />
{props.rule.description}
</span>
<span>
<FontAwesomeIcon icon={props.isCollapseOpen ? faChevronDown : faChevronUp}/>
</span>
</button>
<Collapse
className='collapse-comp'
isOpen={props.isCollapseOpen}
render={renderRule}/>
</div>
);
}
function getRuleIcon() {
let ruleStatus = getRuleStatus()
switch(ruleStatus) {
case STATUSES.STATUS_PASSED:
return faCheckCircle;
case STATUSES.STATUS_VERIFY:
return faExclamationCircle;
case STATUSES.STATUS_FAILED:
return faExclamationCircle;
case STATUSES.STATUS_UNEXECUTED:
return faCircle;
}
}
function getDropdownClass(){
let ruleStatus = getRuleStatus()
switch(ruleStatus) {
case STATUSES.STATUS_PASSED:
return "collapse-success";
case STATUSES.STATUS_VERIFY:
return "collapse-warning";
case STATUSES.STATUS_FAILED:
return "collapse-danger";
case STATUSES.STATUS_UNEXECUTED:
return "collapse-default";
}
}
function getRuleStatus(){
if(props.rule.checked_items === 0) {
return STATUSES.STATUS_UNEXECUTED
} else if (props.rule.items.length === 0) {
return STATUSES.STATUS_PASSED
} else if (props.rule.level === RULE_LEVELS.LEVEL_WARNING) {
return STATUSES.STATUS_VERIFY
} else {
return STATUSES.STATUS_FAILED
}
}
function renderRule() {
return <RuleDisplay rule={props.rule} scoutsuite_data={props.scoutsuite_data}/>
}
return getRuleCollapse();
}
ScoutSuiteSingleRuleDropdown.propTypes = {
isCollapseOpen: PropTypes.bool,
rule: PropTypes.object,
scoutsuite_data: PropTypes.object,
toggleCallback: PropTypes.func
};

View File

@ -12,6 +12,7 @@
@import 'components/PreviewPane';
@import 'components/AdvancedMultiSelect';
@import 'components/particle-component/ParticleBackground';
@import 'components/scoutsuite/ResourceDropdown';
// Define custom elements after bootstrap import

View File

@ -5,6 +5,7 @@ $disabled-color: #f2f2f2;
$info-color: #ade3eb;
$default-color: #8c8c8c;
$warning-color: #ffe28d;
$success-color: #adf6a9;
.collapse-item button {
font-size: inherit;
@ -39,6 +40,10 @@ $warning-color: #ffe28d;
}
}
.collapse-success {
background-color: $success-color !important;
}
.collapse-danger {
background-color: $danger-color !important;
}
@ -99,3 +104,7 @@ $warning-color: #ffe28d;
display: inline-block;
min-width: 6em;
}
.rule-collapse svg{
margin-right: 10px;
}

View File

@ -0,0 +1,20 @@
.resource-display {
margin-top: 10px;
}
.resource-display .resource-value-json {
background-color: $gray-200;
padding: 4px;
}
.resource-display .resource-path-contents svg {
margin-left: 5px;
margin-right: 5px;
width: 10px;
}
.resource-display .resource-value-title,
.resource-display .resource-path-title {
font-weight: 500;
margin-bottom: 0;
}

View File

@ -0,0 +1,21 @@
.scoutsuite-rule-display .description h3{
font-size: 1.2em;
margin-top: 10px;
}
.scoutsuite-rule-display p{
display: inline-block;
}
.scoutsuite-rule-display .checked-resources-title,
.scoutsuite-rule-display .flagged-resources-title,
.scoutsuite-rule-display .reference-list-title{
font-weight: 500;
margin-right: 5px;
margin-bottom: 0;
}
.scoutsuite-rule-display .reference-list a {
display: block;
margin-left: 10px;
}

View File

@ -0,0 +1,5 @@
.scoutsuite-rule-modal .modal-dialog{
max-width: 1000px;
top: 0;
padding: 30px;
}