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
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.git
.gitignore
bin
tmp
.DS_Store
26 changes: 26 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

[{Dockerfile,Dockerfile.*}]
indent_size = 4
tab_width = 4

[{Makefile,makefile,GNUmakefile}]
indent_style = tab
indent_size = 4

[Makefile.*]
indent_style = tab
indent_size = 4

[**/*.{go,mod,sum}]
indent_style = tab
indent_size = unset

[**/*.py]
indent_size = 4
11 changes: 11 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
78 changes: 78 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: ci

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: '1.26.2'

- name: Run go vet
run: go vet ./...

- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "The following files are not formatted:"
echo "$unformatted"
exit 1
fi

test-unit:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: '1.26.2'

- name: Run unit tests
run: make test-unit

test-integration:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: '1.26.2'

- name: Run integration tests
run: make test-integration

build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: '1.26.2'

- name: Build
run: make build
29 changes: 29 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: release

on:
push:
branches:
- main

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Bump Version
id: tag_version
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
default_bump: minor
custom_release_rules: bug:patch:Fixes,chore:patch:Chores,docs:patch:Documentation,feat:minor:Features,refactor:minor:Refactors,test:patch:Tests,ci:patch:Development,dev:patch:Development
- name: Create Release
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
body: ${{ steps.tag_version.outputs.changelog }}
26 changes: 26 additions & 0 deletions .github/workflows/semantic-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: semantic-check
on:
pull_request:
types:
- opened
- edited
- synchronize

permissions:
contents: read
pull-requests: read

jobs:
main:
name: Semantic Commit Message Check
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
name: Check PR for Semantic Commit Message
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
requireScope: false
validateSingleCommit: true
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
!**/.gitkeep

tmp/
dist/
.DS_Store

.local/
.env


23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM golang:1.26.2-alpine AS builder

WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server

FROM alpine:3.21

RUN adduser -D -g '' appuser

USER appuser
WORKDIR /app

COPY --from=builder /out/server /app/server

EXPOSE 8080

ENTRYPOINT ["/app/server"]
51 changes: 51 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
COMPOSE ?= docker compose

.PHONY: test test-unit test-integration build run docker-build up down logs demo-send demo health report

test:
go test ./...

test-unit:
go test ./...

test-integration:
go test ./... -run Integration -count=1

build:
go build ./...

run:
go run ./cmd/server

docker-build:
$(COMPOSE) build api

up:
$(COMPOSE) up --build -d api

down:
$(COMPOSE) down -v --remove-orphans

logs:
$(COMPOSE) logs -f api

demo-send:
$(COMPOSE) run --rm demo-sender

demo: up demo-send
$(COMPOSE) logs api

health:
@BASE_PATH="$${BASE_PATH:-/}"; \
URL_PATH="$${BASE_PATH%/}/v1/manage/healthz"; \
if [ -z "$${URL_PATH}" ]; then URL_PATH="/v1/manage/healthz"; fi; \
curl -fsS "http://localhost:8080$${URL_PATH}"

report:
@BASE_PATH="$${BASE_PATH:-/}"; \
URL_PATH="$${BASE_PATH%/}/v1/reports"; \
if [ -z "$${URL_PATH}" ]; then URL_PATH="/v1/reports"; fi; \
curl -i -X POST \
-H 'Content-Type: application/reports+json' \
--data-binary @demo/reports.json \
"http://localhost:8080$${URL_PATH}"
125 changes: 125 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,128 @@
# browser-reporting-api

Simple, self-hosted Go service for browser Reporting API ingestion.

## General

### What

`browser-reporting-api` is an HTTP service that receives browser reporting
payloads (`application/reports+json` and legacy `application/csp-report` from
`report-uri`), validates each report entry, and streams accepted entries to
stdout as NDJSON (one JSON object per line).

The payload format and reporting behavior align with the browser Reporting API
documented by
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Reporting_API) and
[Chrome](https://developer.chrome.com/docs/capabilities/web-apis/reporting-api).

Endpoints:

- `POST /v1/reports` (or `{BASE_PATH}/v1/reports`)
- `GET /v1/manage/healthz`
- `GET /v1/manage/readyz`

### Why

- Simple to run and reason about
- Safe enough for ingestion (size limits, content-type checks, per-entry
vetting)
- Easy to self-host and observe (accepted reports stream to stdout)
- Collected browser reports (including CSP report traffic) may support
client-side monitoring and script-governance such as those relevant to PCI DSS
payment-page security guidance for Requirements 6.4.3 and 11.6.1 from the
[PCI Security Standards Council](https://blog.pcisecuritystandards.org/new-information-supplement-payment-page-security-and-preventing-e-skimming)

### How

Run locally with Go:

```bash
go run ./cmd/server
```

Run a local demo with Docker Compose:

```bash
make demo
```

This starts the API, sends a sample batched report payload, and prints API logs.
To keep watching streamed report lines:

```bash
make logs
```

The demo sender payload is stored at `demo/reports.json`.

Send a manual sample report:

```bash
make report
```

`make report` sends the same payload file used by the compose demo:
`demo/reports.json`.

Health check:

```bash
make health
```

Stop containers:

```bash
make down
```

Environment variables:

- `LISTEN_ADDR` (default `:8080`)
- `BASE_PATH` (default `/`)
- `MAX_BODY_BYTES` (default `1048576`)
- `REPORTS_ALLOWED_ORIGINS` (default `*`)

Allowed origin examples:

- `REPORTS_ALLOWED_ORIGINS=*`
- `REPORTS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com`
- `REPORTS_ALLOWED_ORIGINS=https://*.example.com,http://localhost:*`

If `BASE_PATH=/collector`, endpoints become:

- `POST /collector/v1/reports`
- `GET /collector/v1/manage/healthz`
- `GET /collector/v1/manage/readyz`

## Development

Run tests:

```bash
go test ./...
```

Make targets used during development:

```bash
make test
make run
make up
make demo-send
make demo
make logs
make report
make health
make down
```

Implementation notes:

- Routes are mounted under configurable `BASE_PATH`.
- Reporting ingestion accepts batched arrays and processes entries
independently.
- Invalid entries are rejected while valid entries in the same batch are still
accepted.
- Accepted entries are emitted to stdout in NDJSON format.
Loading