Skip to content

Commit 5347706

Browse files
committed
REFACTOR: Removes report data from schema.BaseSchema in favor of models.Report
* Removes the `schemas` module. * Adds a new `report_schemas` package. * Adds three abstract classes for report schemas: * `ReportSchema`: Base of all other schemas * `GenericReportSchema`: Base of Out-of-Band Reporting API incident reports. * `LegacyReportSchema`: Base for legacy incident reports. * Adds a registry for report schemas. * Adds modules to implement CSP and HPKP schemas in both contemporary and legacy arrangements. * Adds a `fallback` module for Out-of-Band Reporting API incident reports that don't match any known schemas (preparation for #17). * Updates other app components accordingly. Closes #15
1 parent c6e70c1 commit 5347706

File tree

16 files changed

+649
-393
lines changed

16 files changed

+649
-393
lines changed

README.md

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,32 +60,6 @@ Run the database migrations:
6060
./manage.py migrate lookout
6161
```
6262

63-
### Step 4
64-
65-
Configure logging in `settings.py`.
66-
67-
```python
68-
LOGGING = {
69-
...
70-
'handlers': {
71-
...
72-
'lookout_db': {
73-
'class': 'lookout.logging.DatabaseLogHandler'
74-
}
75-
},
76-
...
77-
'loggers': {
78-
...
79-
'lookout': {
80-
'handlers': ['lookout_db'],
81-
'propagate': False
82-
}
83-
}
84-
}
85-
```
86-
87-
The class `lookout.logging.DatabaseLogHandler` creates instances of `lookout.models.Report`. This will probably be getting replaced.
88-
8963

9064
## Useful Guides
9165

lookout/admin.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@ def capitalization (word, index):
3030

3131
@admin.register(Report)
3232
class ReportAdmin(admin.ModelAdmin):
33-
date_hierarchy = 'created'
33+
date_hierarchy = 'created_time'
3434

3535
empty_value_display = '<i>[empty]</i>'
3636

37-
list_display = ['created', 'type']
38-
list_filter = ['created', 'type', 'generated']
37+
list_display = ['created_time', 'type']
38+
list_filter = ['created_time', 'incident_time', 'type']
3939

4040
save_on_top = True
4141
actions = None
4242

4343
fieldsets = [
4444
[None, {
45-
'fields': ['created', 'generated', 'url']
45+
'fields': ['created_time', 'incident_time', 'type', 'url']
4646
}],
47-
["Body", {
47+
["Details", {
4848
'description': "The report's full contents.",
4949
'fields': ['pretty_body'],
5050
}]
@@ -53,8 +53,11 @@ class ReportAdmin(admin.ModelAdmin):
5353

5454
def get_readonly_fields (self, request, obj=None):
5555
""" Marks all fields as read-only. """
56-
fields = self.fields or [f.name for f in self.model._meta.fields]
57-
return fields + ['pretty_body']
56+
# Don't use self.get_fields, as that causes an infinite recursion
57+
fields = self.fields or []
58+
for fieldset in self.fieldsets or []:
59+
fields.extend(fieldset[1].get('fields', []))
60+
return fields
5861

5962

6063
def has_add_permission (self, request):

lookout/logging.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
from logging import Handler
21

3-
4-
__all__ = ['ReportMessage', 'DatabaseLogHandler']
2+
__all__ = ['ReportMessage']
53

64

75

@@ -20,28 +18,7 @@ def __init__ (self, msg=None, report=None):
2018
self.report = report
2119

2220

23-
2421
def __str__ (self):
2522
""" Returns a plaintext log entry that includes the most important data. """
2623

2724
return '{}\n{}'.format(self.msg, self.report)
28-
29-
30-
31-
class DatabaseLogHandler (Handler):
32-
""" Inserts standard reports into the database. """
33-
34-
def emit (self, record):
35-
from .models import Report
36-
37-
try:
38-
# Attempt to retrieve the report from the message
39-
report = record.msg.report
40-
except AttributeError:
41-
# Don't do anything if it isn't a ReportMessage object
42-
# TODO Raise an exception
43-
return
44-
else:
45-
# Create the model instance
46-
Report.objects.create_from_schema(report)
47-
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
('lookout', '0001_initial'),
8+
]
9+
10+
operations = [
11+
migrations.RenameField(
12+
model_name='report',
13+
old_name='created',
14+
new_name='created_time'
15+
),
16+
migrations.AlterField(
17+
model_name='report',
18+
name='created_time',
19+
field=models.DateTimeField(
20+
primary_key=True, serialize=False, auto_now_add=True,
21+
help_text='When the incident report was submitted.',
22+
verbose_name='Submission Time'
23+
)
24+
),
25+
migrations.RenameField(
26+
model_name='report',
27+
old_name='generated',
28+
new_name='incident_time'
29+
),
30+
migrations.AlterField(
31+
model_name='report',
32+
name='incident_time',
33+
field=models.DateTimeField(db_index=True, help_text='When the incident occurred.')
34+
),
35+
migrations.AlterField(
36+
model_name='report',
37+
name='type',
38+
field=models.CharField(
39+
choices=[
40+
('misc', 'Generic HTTP Reporting API incident report'),
41+
('csp', 'Content Security Policy Report'),
42+
('hpkp', 'HTTP Public Key Pinning Report')
43+
],
44+
db_index=True,
45+
help_text="The report's category.",
46+
max_length=120
47+
)
48+
),
49+
migrations.AlterField(
50+
model_name='report',
51+
name='body',
52+
field=models.TextField(help_text='The contents of the incident report.'),
53+
),
54+
migrations.AlterModelOptions(
55+
name='report',
56+
options={'ordering': ['-incident_time']},
57+
),
58+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.db import migrations, models
2+
import uuid
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
('lookout', '0002_auto_20171008_2238'),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name='report',
14+
name='uuid',
15+
field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, editable=False),
16+
),
17+
migrations.AlterField(
18+
model_name='report',
19+
name='created_time',
20+
field=models.DateTimeField(auto_now_add=True, db_index=True, help_text='When the incident report was submitted.', verbose_name='Submission Time'),
21+
),
22+
]

lookout/models.py

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import json
3+
import uuid
34

45
from datetime import timedelta
56

@@ -12,6 +13,8 @@
1213
from pygments.lexers.data import JsonLexer
1314
from pygments.formatters.html import HtmlFormatter
1415

16+
from .report_schemas import get_matching_schema, report_schema_registry
17+
1518

1619
logger = logging.getLogger(__name__)
1720

@@ -21,39 +24,58 @@
2124

2225

2326
class ReportManager (models.Manager):
24-
def create_from_schema (self, report):
25-
""" Converts a parsed JSON report into a model instance. """
26-
27-
# Report data as a dictionary
28-
data = dict(report)
29-
30-
# Use a static datetime object to make sure `created`, `generated`, and `body['age']` are consistent
31-
now = timezone.now()
32-
33-
# Build the model instance
34-
return self.create(
35-
created=now,
36-
type=data['type'],
37-
# Use the report's `age` property to determine when it was generated
38-
generated=now - timedelta(milliseconds=data['age']),
39-
url=data['url'],
40-
# Store the serialized version
41-
body=str(report)
42-
)
27+
def create_from_json (self, report_json):
28+
""" Converts JSON data into a list of Report instances. """
29+
30+
logger.debug("Decoding JSON")
31+
report_datum = json.loads(report_json)
32+
33+
# Wrap single reports in a list
34+
if not isinstance(report_datum, list):
35+
report_datum = [report_datum]
36+
37+
# Iterate over separate reports
38+
for report_data in report_datum:
39+
logger.debug("Attempt to determine the type of report by testing each schema.")
40+
41+
# Figure out what type of report it is
42+
schema = get_matching_schema(report_data)
43+
44+
# Normalize to a generic schema
45+
schema, report_data = schema.normalize(report_data)
46+
47+
# Use a static datetime object to make sure `created_time`, `incident_time`, and `body['age']` are consistent
48+
now = timezone.now()
49+
50+
# Build the model instance
51+
yield self.create(
52+
created_time=now,
53+
# Use the report's `age` property to determine when the incident occurred
54+
incident_time=now - timedelta(milliseconds=report_data['age']),
55+
type=schema.type,
56+
url=report_data['url'],
57+
body=json.dumps(report_data)
58+
)
59+
4360

61+
report_types = [(schema.type, schema.name) for schema in report_schema_registry.values()]
4462

4563

4664
class Report (models.Model):
4765
""" A report filed through the HTTP Reporting API. """
4866

4967
objects = ReportManager()
5068

51-
created = models.DateTimeField(auto_now_add=True, primary_key=True)
52-
53-
type = models.CharField(max_length=120, help_text="The report's category.")
54-
generated = models.DateTimeField(help_text="The time at which the report was generated.")
69+
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
70+
created_time = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="Submission Time", help_text="When the incident report was submitted.")
71+
incident_time = models.DateTimeField(db_index=True, help_text="When the incident occurred.")
72+
type = models.CharField(max_length=120, db_index=True, choices=report_types, help_text="The report's category.")
5573
url = models.URLField(help_text="The address of the document or worker from which the report was generated.")
56-
body = models.TextField(help_text="The contents of the report.")
74+
body = models.TextField(help_text="The contents of the incident report.")
75+
76+
77+
class Meta:
78+
ordering = ['-incident_time']
5779

5880

5981
def pretty_body (self):
@@ -68,12 +90,14 @@ def pretty_body (self):
6890
return mark_safe(response)
6991

7092

71-
72-
class Meta:
73-
ordering = ['-created']
93+
@property
94+
def schema (self):
95+
return report_schema_registry.get(self.type)
7496

7597

7698
def __str__ (self):
77-
return "{} report from {}".format(self.type.capitalize(), formats.date_format(self.created, 'SHORT_DATETIME_FORMAT'))
78-
79-
99+
""" Something like " """
100+
return "{} report from {}".format(
101+
self.schema.name,
102+
formats.date_format(self.incident_time, 'SHORT_DATETIME_FORMAT')
103+
)

lookout/report_schemas/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .base import *
2+
from .csp import *
3+
from .hpkp import *
4+
from .fallback import *

0 commit comments

Comments
 (0)