Skip to content

Commit

Permalink
Merge branch '5-prometheus-metrics' into 'master'
Browse files Browse the repository at this point in the history
#5 Add prometheus metrics

See merge request ix.ai/csp!7
  • Loading branch information
tlex committed Dec 6, 2020
2 parents 9bce9d3 + d394818 commit faea494
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 11 deletions.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,17 @@ services:
labels:
traefik.enable: 'true'
traefik.http.routers.csp.entrypoints: http,https
traefik.http.routers.csp.service: csp@docker
traefik.http.routers.csp.rule: "Host(`csp.example.com`)"
traefik.http.routers.csp.rule: "Host(`csp.example.com`) && Path(`/`)"
traefik.http.routers.csp.tls.certResolver: 'default'
traefik.http.routers.csp-metrics.entrypoints: http,https
traefik.http.routers.csp-metrics.rule: "Host(`csp.example.com`) && Path(`/metrics`)"
traefik.http.routers.csp-metrics.middlewares: auth
traefik.http.routers.csp-metrics.tls.certResolver: 'default'
traefik.http.services.csp.loadbalancer.server.port: '9180'
environment:
CSP_PATH: '/'
HEALTHZ_PATH: '/health'
ENABLE_METRICS: 'yes'
[...]
my-website:
deploy:
Expand Down Expand Up @@ -114,14 +118,43 @@ Various errors (with `LOGLEVEL:DEBUG`):
2020-12-06 14:54:07.616 DEBUG [csp.log_csp] Content is not JSON: `{"ab": e2}`
```

## Metrics

When setting `ENABLE_METRICS=yes`, the following metrics are exposed:
```
# HELP csp_valid_violation_reports_total Counts the number of valid violation reports
# TYPE csp_valid_violation_reports_total counter
csp_valid_violation_reports_total{blocked_uri="inline",document_uri="https://xxxREDACTEDxxx/",line_number="925",original_policy="upgrade-insecure-requests; default-src self https://cdnjs.cloudflare.com; script-src self https://cdnjs.cloudflare.com https://s.ytimg.com; font-src https://fonts.gstatic.com https://cdnjs.cloudflare.com; report-uri https://csp.example.com/csp;",violated_directive="script-src-elem"} 3.0
# HELP csp_valid_violation_reports_created Counts the number of valid violation reports
# TYPE csp_valid_violation_reports_created gauge
csp_valid_violation_reports_created{blocked_uri="inline",document_uri="https://xxxREDACTEDxxx/",line_number="925",original_policy="upgrade-insecure-requests; default-src self https://cdnjs.cloudflare.com; script-src self https://cdnjs.cloudflare.com https://s.ytimg.com; font-src https://fonts.gstatic.com https://cdnjs.cloudflare.com; report-uri https://csp.example.com/csp;",violated_directive="script-src-elem"} 1.607272996845561e+09
# HELP csp_invalid_violation_reports_total Counts the number of invalid violation reports
# TYPE csp_invalid_violation_reports_total counter
csp_invalid_violation_reports_total{reason="non-csp"} 2.0
csp_invalid_violation_reports_total{reason="non-json"} 1.0
csp_invalid_violation_reports_total{reason="empty"} 1.0
csp_invalid_violation_reports_total{reason="too-large"} 2.0
# HELP csp_invalid_violation_reports_created Counts the number of invalid violation reports
# TYPE csp_invalid_violation_reports_created gauge
csp_invalid_violation_reports_created{reason="non-csp"} 1.60727299902503e+09
csp_invalid_violation_reports_created{reason="non-json"} 1.607273003925279e+09
csp_invalid_violation_reports_created{reason="empty"} 1.607273008638792e+09
csp_invalid_violation_reports_created{reason="too-large"} 1.607273014508008e+09
# HELP csp_version_info Information about CSP
# TYPE csp_version_info gauge
csp_version_info{version="0.2.0-225909200"} 1.0
```

## Environment

| **Variable** | **Default** | **Description** |
|:-------------------------|:-----------:|:-----------------------------------------------------------------------|
| `MAX_CONTENT_LENGTH` | `32768` | The maximum content length (in bytes) of the HTTP POST content |
| `ENABLE_HEALTHZ_VERSION` | `no` | Set this to `yes` to show the version on the `HEALTHZ_PATH` endpoint |
| `ENABLE_METRICS` | `no` | Set this to `yes` to enable the Prometheus metrics |
| `CSP_PATH` | `/csp` | The path used for the CSP reporting |
| `HEALTHZ_PATH` | `/healthz` | The path used for the healthcheck |
| `METRICS_PATH` | `/metrics` | The path used for the the Prometheus metrics |
| `LOGLEVEL` | `INFO` | [Logging Level](https://docs.python.org/3/library/logging.html#levels) |
| `GELF_HOST` | - | If set, GELF UDP logging to this host will be enabled |
| `GELF_PORT` | `12201` | Ignored, if `GELF_HOST` is unset. The UDP port for GELF logging |
Expand Down
4 changes: 3 additions & 1 deletion csp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from . import csp
from .lib import helpers
from .lib import constants
from .lib import prometheus

log = logging.getLogger('csp')

Expand All @@ -15,6 +16,7 @@
'address': 'string',
'port': 'int',
'enable_healthz_version': 'boolean',
'enable_metrics': 'boolean',
'csp_path': 'string',
'healthz_path': 'string',
'metrics_path': 'string',
Expand All @@ -23,5 +25,5 @@

version = f'{constants.VERSION}-{constants.BUILD}'
log.warning(f"Starting **{__package__} {version}**. Listening on {c.get_address()}:{c.get_port()}")

prometheus.PROM_VERSION_INFO.info({'version': f'{version}'})
c.start()
49 changes: 44 additions & 5 deletions csp/csp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from flask import Flask
from flask import request
from waitress import serve
from .lib.constants import VERSION, BUILD, W001, W002, W003
from .lib.constants import VERSION, BUILD, W001, W002, W003, W004
from .lib import prometheus
log = logging.getLogger('csp')


Expand All @@ -19,9 +20,10 @@ class CSP():
'address': '*',
'port': 9180,
'enable_healthz_version': False,
'enable_metrics': False,
'csp_path': '/csp',
'healthz_path': '/healthz',
'metrics_path': '/metrics', # Placeholder
'metrics_path': '/metrics',
}

def __init__(self, **kwargs):
Expand All @@ -34,6 +36,8 @@ def __init__(self, **kwargs):
self.server.secret_key = os.urandom(64).hex()
self.server.add_url_rule(self.settings['csp_path'], 'csp', self.log_csp, methods=['POST'])
self.server.add_url_rule(self.settings['healthz_path'], 'healthz', self.healthz, methods=['GET'])
if self.settings['enable_metrics']:
self.server.add_url_rule(self.settings['metrics_path'], 'metrics', self.metrics, methods=['GET', 'POST'])

def log_csp(self):
""" Logs the content posted """
Expand All @@ -46,23 +50,50 @@ def log_csp(self):
if request.content_length > self.settings['max_content_length']:
log.warning(f'{W001} ({request.content_length}). Dropping.')
result = (W001, 413)
prometheus.PROM_INVALID_VIOLATION_REPORTS_COUNTER.labels(reason='too-large').inc(1)
except TypeError:
log.warning(W002)
result = (W002, 422)
prometheus.PROM_INVALID_VIOLATION_REPORTS_COUNTER.labels(reason='empty').inc(1)

if result == ('OK', 200):
log.debug(f"{request.environ}")
content = request.get_data(as_text=True)
try:
json_content = json.loads(content)
log.info(f'{json.dumps(json_content)}')
# Placeholder: json_content can now be analysed. Ideally, with a function outside of the CSP class
log.info(self.__process_csp(json.loads(content)))
except CSPError:
log.debug(f'{W004}: `{content}`')
log.warning(W004)
result = (W004, 422)
prometheus.PROM_INVALID_VIOLATION_REPORTS_COUNTER.labels(reason='non-csp').inc(1)
except json.decoder.JSONDecodeError:
log.debug(f'{W003}: `{content}`')
log.warning(W003)
result = (W003, 422)
prometheus.PROM_INVALID_VIOLATION_REPORTS_COUNTER.labels(reason='non-json').inc(1)

return result

def __process_csp(self, content):
""" Takes the JSON content and creates the metrics for it """
try:
report = content['csp-report']
except KeyError:
raise CSPError from KeyError

try:
prometheus.PROM_VALID_VIOLATION_REPORTS_COUNTER.labels(
blocked_uri=report['blocked-uri'],
document_uri=report['document-uri'],
original_policy=report['original-policy'],
violated_directive=report['violated-directive'],
line_number=report.get('line-number', 0),
source_file=report.get('source-file'),
).inc(1)
except KeyError:
raise CSPError from KeyError
return json.dumps(content)

def healthz(self):
""" Healthcheck """
version_string = f'{__package__} {VERSION}-{BUILD}'
Expand All @@ -74,6 +105,10 @@ def healthz(self):
message = version_string
return (message, 200)

def metrics(self):
""" Prometheus Metrics """
return prometheus.init()

def start(self):
""" Start the web server """
serve(
Expand All @@ -90,3 +125,7 @@ def get_port(self) -> int:
def get_address(self) -> int:
""" returns the configured address from self.settings['port'] """
return self.settings['address']


class CSPError(Exception):
""" The CSP Exception resides here """
7 changes: 4 additions & 3 deletions csp/lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
VERSION = None
BUILD = None

W001 = 'Content too large'
W002 = 'Empty content received'
W003 = 'Content is not JSON'
W001 = 'W001: Content too large'
W002 = 'W002: Empty content received'
W003 = 'W003: Content is not JSON'
W004 = 'W004: Received JSON is not a CSP Violation Report'
28 changes: 28 additions & 0 deletions csp/lib/prometheus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Initializes the prometheus metrics """

from prometheus_client import Counter, Info, make_wsgi_app

# Prometheus metrics
PROM_VALID_VIOLATION_REPORTS_COUNTER = Counter(
'csp_valid_violation_reports', 'Counts the number of valid violation reports', [
'blocked_uri',
'document_uri',
'original_policy',
'violated_directive',
'line_number',
'source_file',
]
)
PROM_INVALID_VIOLATION_REPORTS_COUNTER = Counter(
'csp_invalid_violation_reports', 'Counts the number of invalid violation reports', [
'reason',
]
)
PROM_VERSION_INFO = Info('csp_version', 'Information about CSP')


def init():
""" Initializes Prometheus """
return make_wsgi_app()

0 comments on commit faea494

Please sign in to comment.