fix(ops): harden runtime placeholder guards
This commit is contained in:
@@ -20,6 +20,9 @@ jobs:
|
||||
run: |
|
||||
find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
|
||||
|
||||
- name: Run production inventory placeholder guard self-test
|
||||
run: bash scripts/check_production_inventory_placeholders.sh --self-test
|
||||
|
||||
powershell-analyzer:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
Generated
+2
@@ -743,6 +743,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"detmir-core",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2873,6 +2874,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"detmir-core",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -618,6 +618,29 @@ cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Production readiness checklist:
|
||||
|
||||
- [ ] public defaults are still sanitized: no live hosts, IPs, domains, tokens,
|
||||
evidence paths, case IDs, hashes, or operator home paths in tracked files;
|
||||
- [ ] live values are supplied only through private inventory/env or server-side
|
||||
runtime env;
|
||||
- [ ] enabled Influx exporters have live URL, org, bucket, token and hosts;
|
||||
- [ ] private production config passes:
|
||||
|
||||
```bash
|
||||
scripts/check_production_inventory_placeholders.sh private-config/runtime.env private-config/ansible-vars.yml
|
||||
```
|
||||
|
||||
Run this guard only on private production override files. Public tracked
|
||||
default/example files may intentionally contain `HOST-EXAMPLE` and TEST-NET
|
||||
values for release hygiene.
|
||||
|
||||
- [ ] `ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml --syntax-check` passes;
|
||||
- [ ] `aw-worktime-influx-exporter.service` and `aw-dlp-influx-exporter.service`
|
||||
complete once and write points;
|
||||
- [ ] `detmir-check --json`, `detmir-status --json` and Grafana check are green;
|
||||
- [ ] rollback path for changed binaries/env files is known.
|
||||
|
||||
На Proxmox:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -65,6 +65,103 @@ pub fn parse_utc_rfc3339(value: &str) -> Result<DateTime<Utc>> {
|
||||
.map(|ts| ts.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
pub mod runtime_guard {
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
const TEST_NET_MARKERS: [&str; 3] = ["192.0.2.", "198.51.100.", "203.0.113."];
|
||||
const CONTAINS_PLACEHOLDERS: [&str; 2] = ["HOST-EXAMPLE", "WINDOWS_USER_EXAMPLE"];
|
||||
const EXACT_PLACEHOLDERS: [&str; 9] = [
|
||||
"CHANGE_ME",
|
||||
"CHANGEME",
|
||||
"REPLACE_ME",
|
||||
"REPLACE-ME",
|
||||
"YOUR_TOKEN",
|
||||
"YOUR_API_KEY",
|
||||
"TOKEN",
|
||||
"SECRET",
|
||||
"PASSWORD",
|
||||
];
|
||||
|
||||
pub fn is_runtime_placeholder(value: &str) -> bool {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let normalized = trimmed.to_ascii_uppercase();
|
||||
TEST_NET_MARKERS
|
||||
.iter()
|
||||
.any(|marker| normalized.contains(marker))
|
||||
|| CONTAINS_PLACEHOLDERS
|
||||
.iter()
|
||||
.any(|placeholder| normalized.contains(placeholder))
|
||||
|| EXACT_PLACEHOLDERS
|
||||
.iter()
|
||||
.any(|placeholder| normalized == *placeholder)
|
||||
|| normalized == "EXAMPLE"
|
||||
|| normalized.contains("-EXAMPLE")
|
||||
|| normalized.contains("_EXAMPLE")
|
||||
|| normalized.contains("EXAMPLE.")
|
||||
|| normalized.contains(".EXAMPLE")
|
||||
|| normalized.starts_with("YOUR_")
|
||||
|| (normalized.starts_with('<') && normalized.ends_with('>'))
|
||||
}
|
||||
|
||||
pub fn is_secret_placeholder(value: &str) -> bool {
|
||||
is_runtime_placeholder(value)
|
||||
|| matches!(
|
||||
value.trim().to_ascii_uppercase().as_str(),
|
||||
"API_KEY" | "INFLUX_TOKEN" | "WRITE_TOKEN" | "BEARER_TOKEN"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ensure_runtime_value(name: &str, value: &str, context: &str) -> Result<()> {
|
||||
if is_runtime_placeholder(value) {
|
||||
bail!("{name} contains an empty/example/TEST-NET value while {context}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_secret_value(name: &str, value: &str, context: &str) -> Result<()> {
|
||||
if is_secret_placeholder(value) {
|
||||
bail!("{name} contains an empty/example secret value while {context}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_runtime_values<'a>(
|
||||
name: &str,
|
||||
values: impl IntoIterator<Item = &'a String>,
|
||||
context: &str,
|
||||
) -> Result<()> {
|
||||
let mut empty = true;
|
||||
for value in values {
|
||||
empty = false;
|
||||
ensure_runtime_value(name, value, context)?;
|
||||
}
|
||||
if empty {
|
||||
bail!("{name} contains an empty/example value while {context}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_influx_runtime_config(
|
||||
prefix: &str,
|
||||
url: &str,
|
||||
org: &str,
|
||||
bucket: &str,
|
||||
token: &str,
|
||||
hosts: &[String],
|
||||
) -> Result<()> {
|
||||
let context = format!("{prefix}_ENABLED=true");
|
||||
ensure_runtime_value(&format!("{prefix}_URL"), url, &context)?;
|
||||
ensure_runtime_value(&format!("{prefix}_ORG"), org, &context)?;
|
||||
ensure_runtime_value(&format!("{prefix}_BUCKET"), bucket, &context)?;
|
||||
ensure_secret_value(&format!("{prefix}_TOKEN"), token, &context)?;
|
||||
ensure_runtime_values(&format!("{prefix}_HOSTS"), hosts, &context)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -85,4 +182,45 @@ mod tests {
|
||||
"2026-05-31T10:20:30Z"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_guard_detects_public_placeholders() {
|
||||
assert!(runtime_guard::is_runtime_placeholder("HOST-EXAMPLE"));
|
||||
assert!(runtime_guard::is_runtime_placeholder(
|
||||
"http://192.0.2.10:8086"
|
||||
));
|
||||
assert!(runtime_guard::is_runtime_placeholder("<TOKEN>"));
|
||||
assert!(runtime_guard::is_secret_placeholder("CHANGE_ME"));
|
||||
assert!(!runtime_guard::is_runtime_placeholder("aw_metrics"));
|
||||
assert!(!runtime_guard::is_runtime_placeholder("proxmox"));
|
||||
assert!(!runtime_guard::is_secret_placeholder(
|
||||
"prod-write-token-value"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_guard_validates_full_influx_config() {
|
||||
let hosts = vec!["WINDOWS-HOST".to_string()];
|
||||
runtime_guard::ensure_influx_runtime_config(
|
||||
"AW_WORKTIME_INFLUX",
|
||||
"http://influxdb.internal:8086",
|
||||
"proxmox",
|
||||
"aw_metrics",
|
||||
"prod-write-token-value",
|
||||
&hosts,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = runtime_guard::ensure_influx_runtime_config(
|
||||
"AW_WORKTIME_INFLUX",
|
||||
"http://influxdb.internal:8086",
|
||||
"proxmox",
|
||||
"aw_metrics",
|
||||
"CHANGE_ME",
|
||||
&hosts,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("AW_WORKTIME_INFLUX_TOKEN"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ publish = false
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
detmir-core.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{thread, time::Duration};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use clap::Parser;
|
||||
use detmir_core::runtime_guard::ensure_influx_runtime_config;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
@@ -120,31 +121,18 @@ fn load_config() -> Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_runtime_placeholder(value: &str) -> bool {
|
||||
let normalized = value.trim().to_ascii_uppercase();
|
||||
normalized.is_empty()
|
||||
|| normalized.contains("HOST-EXAMPLE")
|
||||
|| normalized.contains("WINDOWS_USER_EXAMPLE")
|
||||
|| normalized.contains("192.0.2.")
|
||||
|| normalized.contains("198.51.100.")
|
||||
|| normalized.contains("203.0.113.")
|
||||
}
|
||||
|
||||
fn validate_runtime_config(config: &Config) -> Result<()> {
|
||||
if !config.influx_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
if is_runtime_placeholder(&config.influx_url) {
|
||||
bail!(
|
||||
"AW_DLP_INFLUX_URL contains an empty/example/TEST-NET value while AW_DLP_INFLUX_ENABLED=true"
|
||||
);
|
||||
}
|
||||
if config.hosts.is_empty() || config.hosts.iter().any(|host| is_runtime_placeholder(host)) {
|
||||
bail!(
|
||||
"AW_DLP_INFLUX_HOSTS contains an empty/example value while AW_DLP_INFLUX_ENABLED=true"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
ensure_influx_runtime_config(
|
||||
"AW_DLP_INFLUX",
|
||||
&config.influx_url,
|
||||
&config.influx_org,
|
||||
&config.influx_bucket,
|
||||
&config.influx_token,
|
||||
&config.hosts,
|
||||
)
|
||||
}
|
||||
|
||||
fn utc_now() -> DateTime<Utc> {
|
||||
@@ -1056,9 +1044,20 @@ mod tests {
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_DLP_INFLUX_URL"));
|
||||
|
||||
config.influx_url = "http://influxdb.example.internal:8086".to_string();
|
||||
config.influx_url = "http://influxdb.internal:8086".to_string();
|
||||
config.influx_token = "prod-write-token-value".to_string();
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_DLP_INFLUX_HOSTS"));
|
||||
|
||||
config.hosts = vec!["WINDOWS-HOST".to_string()];
|
||||
config.influx_bucket = "BUCKET-EXAMPLE".to_string();
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_DLP_INFLUX_BUCKET"));
|
||||
|
||||
config.influx_bucket = DEFAULT_INFLUX_BUCKET.to_string();
|
||||
config.influx_token = "CHANGE_ME".to_string();
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_DLP_INFLUX_TOKEN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -10,6 +10,7 @@ publish = false
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
detmir-core.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{collections::HashMap, thread, time::Duration};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeDelta, TimeZone, Timelike, Utc};
|
||||
use clap::Parser;
|
||||
use detmir_core::runtime_guard::ensure_influx_runtime_config;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
@@ -204,31 +205,18 @@ fn load_config() -> Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_runtime_placeholder(value: &str) -> bool {
|
||||
let normalized = value.trim().to_ascii_uppercase();
|
||||
normalized.is_empty()
|
||||
|| normalized.contains("HOST-EXAMPLE")
|
||||
|| normalized.contains("WINDOWS_USER_EXAMPLE")
|
||||
|| normalized.contains("192.0.2.")
|
||||
|| normalized.contains("198.51.100.")
|
||||
|| normalized.contains("203.0.113.")
|
||||
}
|
||||
|
||||
fn validate_runtime_config(config: &Config) -> Result<()> {
|
||||
if !config.influx_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
if is_runtime_placeholder(&config.influx_url) {
|
||||
bail!(
|
||||
"AW_WORKTIME_INFLUX_URL contains an empty/example/TEST-NET value while AW_WORKTIME_INFLUX_ENABLED=true"
|
||||
);
|
||||
}
|
||||
if config.hosts.is_empty() || config.hosts.iter().any(|host| is_runtime_placeholder(host)) {
|
||||
bail!(
|
||||
"AW_WORKTIME_INFLUX_HOSTS contains an empty/example value while AW_WORKTIME_INFLUX_ENABLED=true"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
ensure_influx_runtime_config(
|
||||
"AW_WORKTIME_INFLUX",
|
||||
&config.influx_url,
|
||||
&config.influx_org,
|
||||
&config.influx_bucket,
|
||||
&config.influx_token,
|
||||
&config.hosts,
|
||||
)
|
||||
}
|
||||
|
||||
fn utc_now() -> DateTime<Utc> {
|
||||
@@ -1042,9 +1030,20 @@ mod tests {
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_WORKTIME_INFLUX_URL"));
|
||||
|
||||
config.influx_url = "http://influxdb.example.internal:8086".to_string();
|
||||
config.influx_url = "http://influxdb.internal:8086".to_string();
|
||||
config.influx_token = "prod-write-token-value".to_string();
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_WORKTIME_INFLUX_HOSTS"));
|
||||
|
||||
config.hosts = vec!["WINDOWS-HOST".to_string()];
|
||||
config.influx_bucket = "BUCKET-EXAMPLE".to_string();
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_WORKTIME_INFLUX_BUCKET"));
|
||||
|
||||
config.influx_bucket = DEFAULT_INFLUX_BUCKET.to_string();
|
||||
config.influx_token = "CHANGE_ME".to_string();
|
||||
let err = validate_runtime_config(&config).unwrap_err().to_string();
|
||||
assert!(err.contains("AW_WORKTIME_INFLUX_TOKEN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -611,6 +611,7 @@
|
||||
that:
|
||||
- aw_effective_worktime_influx_token is defined
|
||||
- aw_effective_worktime_influx_token | length > 0
|
||||
- (aw_effective_worktime_influx_token | string | lower | regex_search('^(change_me|changeme|replace-me|replace_me|token|secret|password|api_key|influx_token|write_token|your_.*|<.*>)$')) is none
|
||||
fail_msg: "aw_worktime_influx_enabled=true, но token пуст и в локальном env, и в текущем /etc/activitywatch/aw-server.env. Exporter будет падать и Grafana не получит worktime-ряды."
|
||||
when: aw_worktime_influx_enabled | default(false) | bool
|
||||
|
||||
@@ -619,6 +620,7 @@
|
||||
that:
|
||||
- aw_effective_dlp_influx_token is defined
|
||||
- aw_effective_dlp_influx_token | length > 0
|
||||
- (aw_effective_dlp_influx_token | string | lower | regex_search('^(change_me|changeme|replace-me|replace_me|token|secret|password|api_key|influx_token|write_token|your_.*|<.*>)$')) is none
|
||||
fail_msg: "aw_dlp_influx_enabled=true, но token пуст и в локальном env, и в текущем /etc/activitywatch/aw-server.env. Exporter будет падать и Grafana не получит DLP-ряды."
|
||||
when: aw_dlp_influx_enabled | default(false) | bool
|
||||
|
||||
@@ -629,10 +631,14 @@
|
||||
- "'192.0.2.' not in (aw_worktime_influx_url | default('') | string)"
|
||||
- "'198.51.100.' not in (aw_worktime_influx_url | default('') | string)"
|
||||
- "'203.0.113.' not in (aw_worktime_influx_url | default('') | string)"
|
||||
- (aw_worktime_influx_org | default('') | string | length) > 0
|
||||
- (aw_worktime_influx_org | default('') | string | lower | regex_search('example|change_me|changeme|replace-me|replace_me|your_|<|>')) is none
|
||||
- (aw_worktime_influx_bucket | default('') | string | length) > 0
|
||||
- (aw_worktime_influx_bucket | default('') | string | lower | regex_search('example|change_me|changeme|replace-me|replace_me|your_|<|>')) is none
|
||||
- (aw_worktime_influx_hosts | default('') | string | length) > 0
|
||||
- "'HOST-EXAMPLE' not in (aw_worktime_influx_hosts | default('') | string)"
|
||||
- "'WINDOWS_USER_EXAMPLE' not in (aw_worktime_influx_hosts | default('') | string)"
|
||||
fail_msg: "aw_worktime_influx_enabled=true, но URL/hosts похожи на public example/TEST-NET значения. Задайте live значения в private inventory/env, не в public repo."
|
||||
fail_msg: "aw_worktime_influx_enabled=true, но URL/org/bucket/hosts похожи на public example/TEST-NET значения. Задайте live значения в private inventory/env, не в public repo."
|
||||
when: aw_worktime_influx_enabled | default(false) | bool
|
||||
|
||||
- name: Проверить destination для AW DLP Influx exporter
|
||||
@@ -642,10 +648,14 @@
|
||||
- "'192.0.2.' not in (aw_dlp_influx_url | default('') | string)"
|
||||
- "'198.51.100.' not in (aw_dlp_influx_url | default('') | string)"
|
||||
- "'203.0.113.' not in (aw_dlp_influx_url | default('') | string)"
|
||||
- (aw_dlp_influx_org | default('') | string | length) > 0
|
||||
- (aw_dlp_influx_org | default('') | string | lower | regex_search('example|change_me|changeme|replace-me|replace_me|your_|<|>')) is none
|
||||
- (aw_dlp_influx_bucket | default('') | string | length) > 0
|
||||
- (aw_dlp_influx_bucket | default('') | string | lower | regex_search('example|change_me|changeme|replace-me|replace_me|your_|<|>')) is none
|
||||
- (aw_dlp_influx_hosts | default('') | string | length) > 0
|
||||
- "'HOST-EXAMPLE' not in (aw_dlp_influx_hosts | default('') | string)"
|
||||
- "'WINDOWS_USER_EXAMPLE' not in (aw_dlp_influx_hosts | default('') | string)"
|
||||
fail_msg: "aw_dlp_influx_enabled=true, но URL/hosts похожи на public example/TEST-NET значения. Задайте live значения в private inventory/env, не в public repo."
|
||||
fail_msg: "aw_dlp_influx_enabled=true, но URL/org/bucket/hosts похожи на public example/TEST-NET значения. Задайте live значения в private inventory/env, не в public repo."
|
||||
when: aw_dlp_influx_enabled | default(false) | bool
|
||||
|
||||
- name: Записать /etc/activitywatch/aw-server.env перед хотфиксами
|
||||
|
||||
@@ -36,8 +36,11 @@ Grafana datasource `InfluxDB-AW` читает тот же bucket.
|
||||
|
||||
- если `aw_worktime_influx_enabled=true`, `aw_worktime_influx_token` обязан быть непустым;
|
||||
- если `aw_dlp_influx_enabled=true`, `aw_dlp_influx_token` обязан быть непустым.
|
||||
- если exporter включен, Influx URL и список hosts не могут быть пустыми,
|
||||
`HOST-EXAMPLE`, `WINDOWS_USER_EXAMPLE` или TEST-NET адресами.
|
||||
- если exporter включен, Influx URL, org, bucket и список hosts не могут быть
|
||||
пустыми, `HOST-EXAMPLE`, `WINDOWS_USER_EXAMPLE` или TEST-NET/example
|
||||
значениями;
|
||||
- token не может быть пустым или placeholder вроде `CHANGE_ME`, `TOKEN`,
|
||||
`YOUR_*`, `<TOKEN>`.
|
||||
|
||||
Кроме того, разовый запуск exporters больше не маскируется `failed_when: false`. Если запись в Influx сломана, playbook должен явно упасть, а не оставлять Grafana со старыми рядами.
|
||||
|
||||
@@ -59,9 +62,10 @@ aw_dlp_influx_hosts: "HOST-EXAMPLE"
|
||||
Защита от повторения ошибки:
|
||||
|
||||
- Ansible не даст записать `/etc/activitywatch/aw-server.env`, если включенный
|
||||
exporter получил public placeholder вместо live destination;
|
||||
exporter получил public placeholder вместо live destination/token;
|
||||
- Rust exporter дополнительно завершится с понятной ошибкой по переменной
|
||||
`AW_*_INFLUX_URL` или `AW_*_INFLUX_HOSTS`, если такой env все же попал в
|
||||
`AW_*_INFLUX_URL`, `AW_*_INFLUX_ORG`, `AW_*_INFLUX_BUCKET`,
|
||||
`AW_*_INFLUX_TOKEN` или `AW_*_INFLUX_HOSTS`, если такой env все же попал в
|
||||
runtime;
|
||||
- живые URL, hosts и tokens не переносятся в tracked files.
|
||||
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/check_production_inventory_placeholders.sh [--allow-missing] FILE...
|
||||
DETMIR_PRODUCTION_CONFIG_PATHS="file1:file2" scripts/check_production_inventory_placeholders.sh
|
||||
scripts/check_production_inventory_placeholders.sh --self-test
|
||||
|
||||
Fails when production inventory/env files contain public placeholder values:
|
||||
TEST-NET addresses, HOST-EXAMPLE, WINDOWS_USER_EXAMPLE, CHANGE_ME, YOUR_*,
|
||||
replace-me, or angle-bracket placeholders.
|
||||
EOF
|
||||
}
|
||||
|
||||
pattern='(192\.0\.2\.|198\.51\.100\.|203\.0\.113\.|HOST-EXAMPLE|WINDOWS_USER_EXAMPLE|CHANGE_ME|CHANGEME|REPLACE_ME|replace-me|YOUR_[A-Z0-9_]*|<[A-Z0-9_ -]+>)'
|
||||
allow_missing=0
|
||||
self_test=0
|
||||
paths=()
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--allow-missing)
|
||||
allow_missing=1
|
||||
;;
|
||||
--self-test)
|
||||
self_test=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
paths+=("$1")
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
run_scan() {
|
||||
local file
|
||||
local found=0
|
||||
for file in "$@"; do
|
||||
if [[ ! -e "$file" ]]; then
|
||||
if (( allow_missing == 0 )); then
|
||||
printf 'missing production config path: %s\n' "$file" >&2
|
||||
found=1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [[ -d "$file" ]]; then
|
||||
while IFS= read -r -d '' child; do
|
||||
if grep -nE "$pattern" "$child" >/dev/null; then
|
||||
printf 'placeholder found in %s\n' "$child" >&2
|
||||
grep -nE "$pattern" "$child" | sed -E 's/^([0-9]+):.*/ line \1: placeholder marker/' >&2
|
||||
found=1
|
||||
fi
|
||||
done < <(find "$file" -type f \( -name '*.env' -o -name '*.ini' -o -name '*.yml' -o -name '*.yaml' \) -print0)
|
||||
elif grep -nE "$pattern" "$file" >/dev/null; then
|
||||
printf 'placeholder found in %s\n' "$file" >&2
|
||||
grep -nE "$pattern" "$file" | sed -E 's/^([0-9]+):.*/ line \1: placeholder marker/' >&2
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
return "$found"
|
||||
}
|
||||
|
||||
if (( self_test == 1 )); then
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
good="$tmp_dir/good.env"
|
||||
bad="$tmp_dir/bad.env"
|
||||
cat >"$good" <<'EOF'
|
||||
AW_WORKTIME_INFLUX_URL=http://influxdb.internal:8086
|
||||
AW_WORKTIME_INFLUX_HOSTS=WINDOWS-HOST
|
||||
AW_WORKTIME_INFLUX_TOKEN=prod-write-token-value
|
||||
EOF
|
||||
cat >"$bad" <<'EOF'
|
||||
AW_WORKTIME_INFLUX_URL=http://192.0.2.10:8086
|
||||
AW_WORKTIME_INFLUX_HOSTS=HOST-EXAMPLE
|
||||
AW_WORKTIME_INFLUX_TOKEN=CHANGE_ME
|
||||
EOF
|
||||
run_scan "$good"
|
||||
if run_scan "$bad" >/dev/null 2>&1; then
|
||||
echo "self-test failed: bad fixture was accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "production inventory placeholder guard self-test: OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ((${#paths[@]} == 0)) && [[ -n "${DETMIR_PRODUCTION_CONFIG_PATHS:-}" ]]; then
|
||||
IFS=':' read -r -a paths <<<"$DETMIR_PRODUCTION_CONFIG_PATHS"
|
||||
fi
|
||||
|
||||
if ((${#paths[@]} == 0)); then
|
||||
if (( allow_missing == 1 )); then
|
||||
echo "production inventory placeholder guard: skipped (no production paths)"
|
||||
exit 0
|
||||
fi
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
run_scan "${paths[@]}"
|
||||
echo "production inventory placeholder guard: OK"
|
||||
Reference in New Issue
Block a user