Skip to content

Commit

Permalink
feat(api): Support security interfaces fully
Browse files Browse the repository at this point in the history
- Expose ExpectCT, ExpectStaple, and HPKP in UI
- Correct hashing on ExpectCT, ExpectStaple, and HPKP to include type
- Add support for ExpectCT and HPKP samples
- Add bin/mock-event helper
  • Loading branch information
dcramer committed Apr 26, 2018
1 parent 3030d22 commit 80bb9e0
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 25 deletions.
1 change: 0 additions & 1 deletion bin/load-mocks
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,6 @@ def main(num_events=1, extra_events=False):
},
)[0]


create_sample_event(
project=project,
environment=ENVIRONMENTS.next(),
Expand Down
51 changes: 51 additions & 0 deletions bin/mock-event
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python
# isort:skip_file
from sentry.runner import configure
configure()

import sys

from django.utils import timezone

from sentry.models import Project
from sentry.utils.samples import create_sample_event


import argparse


def main(
project,
sample_type,
):
org_slug, project_slug = project.split('/', 1)

project = Project.objects.get(
organization__slug=org_slug,
slug=project_slug,
)

event = create_sample_event(
project=project,
platform=sample_type,
)
if not event:
sys.stderr.write('ERR: No event created. Was the sample type valid?\n')
sys.exit(1)

if not project.first_event:
project.update(first_event=timezone.now())

print('> Created event {}'.format(event.event_id))


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('project')
parser.add_argument('sample_type')
args = parser.parse_args()

main(
project=args.project,
sample_type=args.sample_type,
)
35 changes: 35 additions & 0 deletions src/sentry/data/samples/expectct.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"message": "Expect-CT failed for 'example.com'",
"expectct": {
"date_time": "2014-04-06T13:00:50Z",
"hostname": "example.com",
"port": 443,
"effective_expiration_date": "2014-05-01T12:40:50Z",
"served_certificate_chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
"validated_certificate_chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
"scts": [
{
"version": 1,
"status": "invalid",
"source": "embedded",
"serialized_sct": "ABCD=="
}
]
},
"sentry.interfaces.Http": {
"url": "https://example.com/welcome/",
"headers": [
[
"Referer",
"https://www.google.com/"
],
[
"User-Agent",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) coc_coc_browser/51.2.109 Chrome/45.2.2454.109 Safari/537.36"
]
]
},
"sentry.interfaces.User": {
"ip_address": "127.0.0.1"
}
}
29 changes: 29 additions & 0 deletions src/sentry/data/samples/hpkp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"message": "Public key pinning validation failed for 'example.com'",
"hpkp": {
"date_time": "2014-04-06T13:00:50Z",
"hostname": "example.com",
"port": 443,
"effective_expiration_date": "2014-05-01T12:40:50Z",
"include_subdomains": false,
"served_certificate_chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
"validated_certificate_chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
"known_pins": ["pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""]
},
"sentry.interfaces.Http": {
"url": "https://example.com/welcome/",
"headers": [
[
"Referer",
"https://www.google.com/"
],
[
"User-Agent",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) coc_coc_browser/51.2.109 Chrome/45.2.2454.109 Safari/537.36"
]
]
},
"sentry.interfaces.User": {
"ip_address": "127.0.0.1"
}
}
6 changes: 3 additions & 3 deletions src/sentry/interfaces/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def get_culprit(self):
return None

def get_hash(self, is_processed_data=True):
return [self.hostname]
return ['hpkp', self.hostname]

def get_message(self):
return "Public key pinning validation failed for '{self.hostname}'".format(self=self)
Expand Down Expand Up @@ -234,7 +234,7 @@ def get_culprit(self):
return self.hostname

def get_hash(self, is_processed_data=True):
return [self.hostname]
return ['expect-staple', self.hostname]

def get_message(self):
return "Expect-Staple failed for '{self.hostname}'".format(self=self)
Expand Down Expand Up @@ -296,7 +296,7 @@ def get_culprit(self):
return self.hostname

def get_hash(self, is_processed_data=True):
return [self.hostname]
return ['expect-ct', self.hostname]

def get_message(self):
return "Expect-CT failed for '{self.hostname}'".format(self=self)
Expand Down
13 changes: 12 additions & 1 deletion src/sentry/static/sentry/app/components/eventOrGroupHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ class EventOrGroupHeader extends React.Component {
data: PropTypes.shape({
id: PropTypes.string,
level: PropTypes.string,
type: PropTypes.oneOf(['error', 'csp', 'default']).isRequired,
type: PropTypes.oneOf([
'error',
'csp',
'hpkp',
'expectct',
'expectstaple',
'default',
]).isRequired,
title: PropTypes.string,
metadata: Metadata,
groupID: PropTypes.string,
Expand All @@ -45,6 +52,10 @@ class EventOrGroupHeader extends React.Component {
return metadata.value;
case 'csp':
return metadata.message;
case 'expectct':
case 'expectstaple':
case 'hpkp':
return '';
default:
return culprit || '';
}
Expand Down
12 changes: 11 additions & 1 deletion src/sentry/static/sentry/app/components/eventOrGroupTitle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import {Metadata} from '../proptypes';
class EventOrGroupTitle extends React.Component {
static propTypes = {
data: PropTypes.shape({
type: PropTypes.oneOf(['error', 'csp', 'default']).isRequired,
type: PropTypes.oneOf([
'error',
'csp',
'hpkp',
'expectct',
'expectstaple',
'default',
]).isRequired,
title: PropTypes.string,
metadata: Metadata.isRequired,
culprit: PropTypes.string,
Expand All @@ -23,6 +30,9 @@ class EventOrGroupTitle extends React.Component {
} else if (type == 'csp') {
title = metadata.directive;
subtitle = metadata.uri;
} else if (type === 'expectct' || type === 'expectstaple' || type === 'hpkp') {
title = metadata.message;
subtitle = metadata.origin;
} else if (type == 'default') {
title = metadata.title;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import StacktraceInterface from './interfaces/stacktrace';
import TemplateInterface from './interfaces/template';
import CspInterface from './interfaces/csp';
import BreadcrumbsInterface from './interfaces/breadcrumbs';
import GenericInterface from './interfaces/generic';
import ThreadsInterface from './interfaces/threads';
import DebugMetaInterface from './interfaces/debugmeta';

Expand All @@ -37,6 +38,9 @@ export const INTERFACES = {
stacktrace: StacktraceInterface,
template: TemplateInterface,
csp: CspInterface,
expectct: GenericInterface,
expectstaple: GenericInterface,
hpkp: GenericInterface,
breadcrumbs: BreadcrumbsInterface,
threads: ThreadsInterface,
debugmeta: DebugMetaInterface,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function getView(view, data) {
}
}

class CSPInterface extends React.Component {
export default class CspInterface extends React.Component {
static propTypes = {
group: SentryTypes.Group.isRequired,
event: SentryTypes.Event.isRequired,
Expand Down Expand Up @@ -91,5 +91,3 @@ class CSPInterface extends React.Component {
);
}
}

export default CSPInterface;
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import SentryTypes from '../../../proptypes';

import GroupEventDataSection from '../eventDataSection';
import KeyValueList from './keyValueList';
import {t} from '../../../locale';
import {objectToArray} from '../../../utils';

function getView(view, data) {
switch (view) {
case 'report':
return <KeyValueList data={objectToArray(data)} isContextData={true} />;
case 'raw':
return <pre>{JSON.stringify({'csp-report': data}, null, 2)}</pre>;
default:
throw new TypeError(`Invalid view: ${view}`);
}
}
export default class GenericInterface extends Component {
static propTypes = {
group: SentryTypes.Group.isRequired,
event: SentryTypes.Event.isRequired,
type: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
};

constructor(props) {
super(props);
let {data} = props;
this.state = {
view: 'report',
data,
};
}

toggleView = value => {
this.setState({
view: value,
});
};

render() {
let {view, data} = this.state;
let {group, event, type} = this.props;

let title = (
<div>
<div className="btn-group">
<a
className={(view === 'report' ? 'active' : '') + ' btn btn-default btn-sm'}
onClick={this.toggleView.bind(this, 'report')}
>
{t('Report')}
</a>
<a
className={(view === 'raw' ? 'active' : '') + ' btn btn-default btn-sm'}
onClick={this.toggleView.bind(this, 'raw')}
>
{t('Raw')}
</a>
</div>
<h3>{t('Report')}</h3>
</div>
);

let children = getView(view, data);

return (
<GroupEventDataSection
group={group}
event={event}
type={type}
title={title}
wrapTitle={false}
>
{children}
</GroupEventDataSection>
);
}
}
4 changes: 4 additions & 0 deletions src/sentry/static/sentry/app/views/groupDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ const GroupDetails = createReactClass({
return group.metadata.type || group.metadata.value;
case 'csp':
return group.metadata.message;
case 'expectct':
case 'expectstaple':
case 'hpkp':
return group.metadata.message;
case 'default':
return group.metadata.title;
default:
Expand Down
16 changes: 8 additions & 8 deletions src/sentry/utils/samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,28 +112,28 @@ def load_data(platform, default=None, timestamp=None, sample_name=None):
if not platform:
continue

try:
sample_name = sample_name or INTEGRATION_ID_TO_PLATFORM_DATA[platform]['name']
except KeyError:
continue

json_path = os.path.join(DATA_ROOT, 'samples', '%s.json' % (platform.encode('utf-8'), ))

if not os.path.exists(json_path):
continue

if not sample_name:
try:
sample_name = INTEGRATION_ID_TO_PLATFORM_DATA[platform]['name']
except KeyError:
pass

with open(json_path) as fp:
data = json.loads(fp.read())
break

if data is None:
return

if platform == 'csp':
if platform in ('csp', 'hkpk', 'expectct', 'expectstaple'):
return data

data['platform'] = platform
data['message'] = 'This is an example %s exception' % (sample_name, )
data['message'] = 'This is an example %s exception' % (sample_name or platform, )
data['sentry.interfaces.User'] = generate_user(
ip_address='127.0.0.1',
username='sentry',
Expand Down
Loading

0 comments on commit 80bb9e0

Please sign in to comment.