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
11 changes: 11 additions & 0 deletions devops/grafana/provisioning/dashboards/dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: 1

providers:
- name: Sorokeep
orgId: 1
folder: Sorokeep
type: file
disableDeletion: true
editable: false
options:
path: /etc/grafana/provisioning/dashboards
39 changes: 39 additions & 0 deletions devops/grafana/provisioning/dashboards/sorokeep-overview.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"annotations": { "list": [] },
"editable": false,
"panels": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "text": "Down" }, "1": { "text": "Up" } }, "type": "value" }
],
"thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] }
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 0 },
"id": 1,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [ { "expr": "up{job=\"sorokeep\"}", "refId": "A" } ],
"title": "Sorokeep Metrics Endpoint",
"type": "stat"
}
],
"schemaVersion": 39,
"tags": [ "sorokeep", "observability" ],
"templating": { "list": [] },
"time": { "from": "now-15m", "to": "now" },
"title": "Sorokeep Overview",
"uid": "sorokeep-overview",
"version": 1
}
10 changes: 10 additions & 0 deletions devops/grafana/provisioning/datasources/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: 1

datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
9 changes: 9 additions & 0 deletions devops/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: sorokeep
static_configs:
- targets:
- sorokeep:9464
47 changes: 47 additions & 0 deletions docker-compose.observability.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Prometheus and Grafana overlay for local Sorokeep observability.
#
# Usage:
# docker compose -f docker-compose.yaml -f docker-compose.observability.yml --profile observability up

services:
sorokeep:
expose:
- "9464"

prometheus:
image: prom/prometheus:v3.3.0
profiles: ["observability"]
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.enable-lifecycle"
ports:
- "${PROMETHEUS_PORT:-9090}:9090"
Comment on lines +17 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '(^|/)docker-compose\.observability\.yml$|docker-compose.*observability|prometheus|grafana'

printf '\nObservability compose snippet:\n'
if [ -f docker-compose.observability.yml ]; then
  nl -ba docker-compose.observability.yml | sed -n '1,120p'
fi

printf '\nGrep lifecycle/port patterns in repo:\n'
rg -n --hidden --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' 'web\.enable-lifecycle|PROMETHEUS_PORT|GRAFANA_PORT|prometheus.*restart|--web\.enable-lifecycle|admin/admin|GF_SECURITY_ADMIN_PASSWORD|GRAFANA_ADMIN|PROMETHEUS_ADMIN' . || true

printf '\nDocker publish port semantics reference from local install docs (if available):\n'
sed -n '17,22p' docker-compose.observability.yml 2>/dev/null || true

Repository: AbdulmalikAlayande/sorokeep

Length of output: 480


🌐 Web query:

Docker Compose ports short syntax published on all interfaces host IP 127.0.0.1 documentation

💡 Result:

In Docker Compose, the short syntax for ports follows the format [HOST:]CONTAINER[/PROTOCOL] [1][2][3]. If you do not specify a host IP in the short syntax, Docker Compose defaults to binding the port to all available network interfaces (0.0.0.0), which may expose the container to the public internet [1][4][3]. To restrict access so that the container is only reachable from the host machine, you must explicitly include 127.0.0.1 in the port mapping [1][4][3]. For example: ports: - "127.0.0.1:8080:80" This configuration maps the container's port 80 to the host's port 8080, but only on the 127.0.0.1 loopback address [1][2][3]. Key considerations include: - Security: Publishing to 0.0.0.0 by default can bypass host firewall rules if not properly managed [1][3]. - YAML Syntax: When using the short syntax with an IP address, it is recommended to wrap the entire string in quotes to prevent the YAML parser from misinterpreting the values (e.g., as a base-60 float) [2][5][6]. - Alternatives: If you need to avoid specifying the IP for every service, you can configure the Docker daemon to use 127.0.0.1 as the default binding address for published ports, though this is a global setting that affects all containers [4].

Citations:


🌐 Web query:

Prometheus --web.enable-lifecycle endpoint security unauthenticated POST /-/reload /-/quit documentation

💡 Result:

When the --web.enable-lifecycle flag is enabled in Prometheus, the /-/reload and /-/quit endpoints become accessible via HTTP requests [1][2]. These endpoints are disabled by default [2][3]. Crucially, Prometheus does not natively enforce authentication on these lifecycle endpoints [1]. If this flag is enabled, any party with network access to the Prometheus HTTP server can trigger a configuration reload or a graceful shutdown of the process [1][4]. To mitigate the security risks associated with enabling these endpoints, the Prometheus documentation recommends the following: 1. Network Isolation: Ensure that the Prometheus server is not exposed to untrusted networks [1]. 2. Reverse Proxy Authentication: If you need to enable these endpoints but require authentication, use an external reverse proxy (e.g., Nginx, Apache, or a dedicated sidecar proxy) to intercept requests and enforce authentication before forwarding them to the Prometheus instance [1]. Alternatively, you can perform these actions without exposing the HTTP endpoints by using system signals: - Reload configuration: Send a SIGHUP signal to the Prometheus process [2][5]. - Shutdown: Send a SIGTERM signal to the Prometheus process [2][5]. Using these signals is the recommended, more secure alternative to using the HTTP lifecycle endpoints when local access to the server is available [2][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("docker-compose.observability.yml")
text = p.read_text()
print("docker-compose.observability.yml lines:")
for i, line in enumerate(text.splitlines(), 1):
    print(f"{i:3}: {line}")

print("\nRelevant patterns:")
for word in ["--web.enable-lifecycle", "PROMETHEUS_PORT", "GRAFANA_PORT", "admin/admin"]:
    print(f"{word}: {word in text}")
PY

printf '\nGrafana-related security/env defaults:\n'
rg -n --hidden --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' 'admin/admin|GF_SECURITY_ADMIN|ADMIN_PASSWORD|PROMETHEUS_PORT|GRAFANA_PORT|--web\.enable-lifecycle|web\.enable-lifecycle' . || true

Repository: AbdulmalikAlayande/sorokeep

Length of output: 2680


Keep the observability stack local by default.

Docker Compose short port mappings publish on all host interfaces, so the Prometheus lifecycle endpoint and Grafana login are reachable externally unless protected. Bind both defaults to 127.0.0.1 and remove --web.enable-lifecycle unless lifecycle controls are required and wrapped in authentication/proxy controls; also require non-default Grafana admin credentials before enabling this overlay.

Proposed hardening
     command:
       - "--config.file=/etc/prometheus/prometheus.yml"
       - "--storage.tsdb.path=/prometheus"
-      - "--web.enable-lifecycle"
...
-      - "${PROMETHEUS_PORT:-9090}:9090"
+      - "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090"
...
-      - "${GRAFANA_PORT:-3000}:3000"
+      - "127.0.0.1:${GRAFANA_PORT:-3000}:3000"

Also applies to lines 33-37.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- "--web.enable-lifecycle"
ports:
- "${PROMETHEUS_PORT:-9090}:9090"
ports:
- "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.observability.yml` around lines 17 - 19, Harden the
observability overlay by binding the default Prometheus and Grafana port
mappings to 127.0.0.1 instead of all host interfaces. Remove
--web.enable-lifecycle from the Prometheus command unless authenticated proxy
protection is present, and require non-default Grafana admin credentials before
enabling the overlay.

volumes:
- ./devops/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
restart: unless-stopped
networks:
- sorokeep-network

grafana:
image: grafana/grafana:11.6.0
profiles: ["observability"]
depends_on:
- prometheus
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
GF_USERS_ALLOW_SIGN_UP: "false"
ports:
- "${GRAFANA_PORT:-3000}:3000"
volumes:
- grafana-data:/var/lib/grafana
- ./devops/grafana/provisioning:/etc/grafana/provisioning:ro
restart: unless-stopped
networks:
- sorokeep-network

volumes:
prometheus-data:
grafana-data:
9 changes: 9 additions & 0 deletions docs/observability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Observability

Run Sorokeep with its local Prometheus and Grafana stack using the observability overlay:

```sh
docker compose -f docker-compose.yaml -f docker-compose.observability.yml --profile observability up
```

Prometheus is available at `http://localhost:9090` and Grafana at `http://localhost:3000`. Grafana uses `admin` / `admin` by default (override them with `GRAFANA_ADMIN_USER` and `GRAFANA_ADMIN_PASSWORD`) and provisions the **Sorokeep Overview** dashboard automatically. Prometheus scrapes the Sorokeep metrics endpoint at `sorokeep:9464` within the Compose network.
36 changes: 36 additions & 0 deletions tests/docker/docker-compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@ import YAML from "yaml";

const ROOT = path.resolve(import.meta.dirname, "../..");
const COMPOSE_FILE = path.join(ROOT, "docker-compose.yaml");
const OBSERVABILITY_COMPOSE_FILE = path.join(ROOT, "docker-compose.observability.yml");
const PROMETHEUS_CONFIG_FILE = path.join(ROOT, "devops/prometheus/prometheus.yml");
const GRAFANA_DASHBOARD_PROVIDER_FILE = path.join(
ROOT,
"devops/grafana/provisioning/dashboards/dashboard.yml",
);
const GRAFANA_DASHBOARD_FILE = path.join(ROOT, "devops/grafana/provisioning/dashboards/sorokeep-overview.json");

let composeConfig: any;
let observabilityComposeConfig: any;

beforeAll(() => {
if (fs.existsSync(COMPOSE_FILE)) {
const raw = fs.readFileSync(COMPOSE_FILE, "utf8");
composeConfig = YAML.parse(raw);
}
if (fs.existsSync(OBSERVABILITY_COMPOSE_FILE)) {
observabilityComposeConfig = YAML.parse(fs.readFileSync(OBSERVABILITY_COMPOSE_FILE, "utf8"));
}
});

describe("docker-compose.yaml configuration", () => {
Expand Down Expand Up @@ -143,3 +154,28 @@ describe("docker-compose.yaml configuration", () => {
});
});
});

describe("observability compose overlay", () => {
it("defines profiled Prometheus and Grafana services", () => {
expect(fs.existsSync(OBSERVABILITY_COMPOSE_FILE)).toBe(true);
expect(observabilityComposeConfig.services.prometheus.profiles).toContain("observability");
expect(observabilityComposeConfig.services.grafana.profiles).toContain("observability");
});

it("wires Prometheus to Sorokeep's metrics endpoint", () => {
expect(fs.existsSync(PROMETHEUS_CONFIG_FILE)).toBe(true);
const prometheusConfig = YAML.parse(fs.readFileSync(PROMETHEUS_CONFIG_FILE, "utf8"));
const targets = prometheusConfig.scrape_configs.flatMap((job: any) =>
job.static_configs.flatMap((config: any) => config.targets),
);
expect(targets).toContain("sorokeep:9464");
});

it("provisions the Sorokeep Grafana dashboard", () => {
expect(fs.existsSync(GRAFANA_DASHBOARD_PROVIDER_FILE)).toBe(true);
expect(fs.existsSync(GRAFANA_DASHBOARD_FILE)).toBe(true);
const dashboard = JSON.parse(fs.readFileSync(GRAFANA_DASHBOARD_FILE, "utf8"));
expect(dashboard.title).toBe("Sorokeep Overview");
expect(dashboard.panels.length).toBeGreaterThan(0);
});
Comment on lines +158 to +180

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Validate the merged Compose configuration, not only individual files.

These tests never run docker compose with both Compose files, so they cannot catch merge-time failures in networks, service overrides, mounts, or profiles. Add a docker compose ... config smoke test (or CI validation) for the actual observability command.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 166-166: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(PROMETHEUS_CONFIG_FILE, "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)


[warning] 176-176: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(GRAFANA_DASHBOARD_FILE, "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/docker/docker-compose.test.ts` around lines 158 - 180, Extend the
observability compose tests around the existing OBSERVABILITY_COMPOSE_FILE and
observabilityComposeConfig checks with a smoke test that runs the actual Docker
Compose command using the base and observability files, including the
observability profile, and executes config. Assert the command succeeds so
merged networks, overrides, mounts, and profiles are validated rather than only
parsing individual files.

});
Loading