@intentius/chant-lexicon-prometheus 0.102.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/collector-metrics.d.ts +58 -0
- package/dist/collector-metrics.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +19 -4
- package/dist/lint/audit-catalog.d.ts +2 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +21 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/prom-helpers.d.ts +9 -2
- package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/prom211.d.ts +12 -0
- package/dist/lint/post-synth/prom211.d.ts.map +1 -0
- package/dist/lint/post-synth/prom212.d.ts +9 -0
- package/dist/lint/post-synth/prom212.d.ts.map +1 -0
- package/dist/lint/post-synth/prom213.d.ts +11 -0
- package/dist/lint/post-synth/prom213.d.ts.map +1 -0
- package/dist/lint/post-synth/prom214.d.ts +9 -0
- package/dist/lint/post-synth/prom214.d.ts.map +1 -0
- package/dist/lint/post-synth/prom215.d.ts +9 -0
- package/dist/lint/post-synth/prom215.d.ts.map +1 -0
- package/dist/lint/post-synth/prom216.d.ts +10 -0
- package/dist/lint/post-synth/prom216.d.ts.map +1 -0
- package/dist/lint/post-synth/prom217.d.ts +9 -0
- package/dist/lint/post-synth/prom217.d.ts.map +1 -0
- package/dist/lint/post-synth/prom218.d.ts +10 -0
- package/dist/lint/post-synth/prom218.d.ts.map +1 -0
- package/dist/lint/post-synth/prom219.d.ts +9 -0
- package/dist/lint/post-synth/prom219.d.ts.map +1 -0
- package/dist/lint/post-synth/prom220.d.ts +9 -0
- package/dist/lint/post-synth/prom220.d.ts.map +1 -0
- package/dist/lint/post-synth/prom221.d.ts +10 -0
- package/dist/lint/post-synth/prom221.d.ts.map +1 -0
- package/dist/lint/post-synth/prom222.d.ts +9 -0
- package/dist/lint/post-synth/prom222.d.ts.map +1 -0
- package/dist/lint/post-synth/prom223.d.ts +10 -0
- package/dist/lint/post-synth/prom223.d.ts.map +1 -0
- package/dist/lint/post-synth/prom224.d.ts +9 -0
- package/dist/lint/post-synth/prom224.d.ts.map +1 -0
- package/dist/lint/post-synth/prom301.d.ts +14 -0
- package/dist/lint/post-synth/prom301.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +15 -0
- package/dist/okf/rules/PROM211.md +11 -0
- package/dist/okf/rules/PROM212.md +11 -0
- package/dist/okf/rules/PROM213.md +11 -0
- package/dist/okf/rules/PROM214.md +11 -0
- package/dist/okf/rules/PROM215.md +11 -0
- package/dist/okf/rules/PROM216.md +11 -0
- package/dist/okf/rules/PROM217.md +11 -0
- package/dist/okf/rules/PROM218.md +11 -0
- package/dist/okf/rules/PROM219.md +11 -0
- package/dist/okf/rules/PROM220.md +11 -0
- package/dist/okf/rules/PROM221.md +11 -0
- package/dist/okf/rules/PROM222.md +11 -0
- package/dist/okf/rules/PROM223.md +11 -0
- package/dist/okf/rules/PROM224.md +11 -0
- package/dist/okf/rules/PROM301.md +11 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-analysis.d.ts +82 -0
- package/dist/promql-analysis.d.ts.map +1 -0
- package/dist/rules/literal-credential.ts +4 -0
- package/dist/rules/prom-helpers.ts +41 -2
- package/dist/rules/prom211.ts +21 -0
- package/dist/rules/prom212.ts +18 -0
- package/dist/rules/prom213.ts +20 -0
- package/dist/rules/prom214.ts +18 -0
- package/dist/rules/prom215.ts +18 -0
- package/dist/rules/prom216.ts +19 -0
- package/dist/rules/prom217.ts +18 -0
- package/dist/rules/prom218.ts +19 -0
- package/dist/rules/prom219.ts +18 -0
- package/dist/rules/prom220.ts +18 -0
- package/dist/rules/prom221.ts +19 -0
- package/dist/rules/prom222.ts +18 -0
- package/dist/rules/prom223.ts +19 -0
- package/dist/rules/prom224.ts +18 -0
- package/dist/rules/prom301.ts +23 -0
- package/dist/validate-config.d.ts +9 -3
- package/dist/validate-config.d.ts.map +1 -1
- package/dist/validate-integrations.d.ts +1 -0
- package/dist/validate-integrations.d.ts.map +1 -1
- package/dist/validate-security.d.ts +13 -0
- package/dist/validate-security.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/collector-metrics.ts +262 -0
- package/src/composites/composites.test.ts +2 -5
- package/src/import/testdata/alertmanager-integrations.yml +5 -1
- package/src/index.ts +1 -0
- package/src/init-templates.test.ts +3 -1
- package/src/lint/audit-catalog.ts +102 -0
- package/src/lint/audit-lineage.test.ts +74 -0
- package/src/lint/audit-lineage.ts +28 -0
- package/src/lint/post-synth/checks.test.ts +257 -0
- package/src/lint/post-synth/index.ts +30 -0
- package/src/lint/post-synth/post-synth.test.ts +3 -1
- package/src/lint/post-synth/prom-helpers.ts +41 -2
- package/src/lint/post-synth/prom211.ts +21 -0
- package/src/lint/post-synth/prom212.ts +18 -0
- package/src/lint/post-synth/prom213.ts +20 -0
- package/src/lint/post-synth/prom214.ts +18 -0
- package/src/lint/post-synth/prom215.ts +18 -0
- package/src/lint/post-synth/prom216.ts +19 -0
- package/src/lint/post-synth/prom217.ts +18 -0
- package/src/lint/post-synth/prom218.ts +19 -0
- package/src/lint/post-synth/prom219.ts +18 -0
- package/src/lint/post-synth/prom220.ts +18 -0
- package/src/lint/post-synth/prom221.ts +19 -0
- package/src/lint/post-synth/prom222.ts +18 -0
- package/src/lint/post-synth/prom223.ts +19 -0
- package/src/lint/post-synth/prom224.ts +18 -0
- package/src/lint/post-synth/prom301.test.ts +157 -0
- package/src/lint/post-synth/prom301.ts +23 -0
- package/src/lint/rules/literal-credential.ts +4 -0
- package/src/lint/rules/rules.test.ts +16 -0
- package/src/plugin.test.ts +22 -0
- package/src/plugin.ts +12 -1
- package/src/promql-analysis.ts +365 -0
- package/src/validate-config.ts +274 -5
- package/src/validate-integrations.ts +1 -1
- package/src/validate-security.ts +143 -0
package/src/validate-config.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `PrometheusRule`'s groups, or on a file parsed by some other tool.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { isValidDuration } from "./duration";
|
|
10
|
+
import { durationMs, formatDuration, isValidDuration } from "./duration";
|
|
11
11
|
import { matcherMatches, parseMatchers, type Matcher } from "./matchers";
|
|
12
12
|
import {
|
|
13
13
|
isAlertingRuleConfig,
|
|
@@ -20,7 +20,20 @@ import {
|
|
|
20
20
|
type RuleGroupConfig,
|
|
21
21
|
} from "./model";
|
|
22
22
|
import { checkPromql } from "./promql";
|
|
23
|
+
import {
|
|
24
|
+
hasCondition,
|
|
25
|
+
histogramProblems,
|
|
26
|
+
isMultiWindow,
|
|
27
|
+
keepsLabel,
|
|
28
|
+
nonCounterRates,
|
|
29
|
+
parsePromql,
|
|
30
|
+
readsOnlyOverTime,
|
|
31
|
+
regexProblems,
|
|
32
|
+
resultLabels,
|
|
33
|
+
selectors,
|
|
34
|
+
} from "./promql-analysis";
|
|
23
35
|
import { validateGlobalSettings, validateReceiverIntegrations } from "./validate-integrations";
|
|
36
|
+
import { validateGlobalSecurity, validateReceiverSecurity } from "./validate-security";
|
|
24
37
|
|
|
25
38
|
export type PrometheusIssueCode =
|
|
26
39
|
| "PROM101"
|
|
@@ -39,7 +52,21 @@ export type PrometheusIssueCode =
|
|
|
39
52
|
| "PROM207"
|
|
40
53
|
| "PROM208"
|
|
41
54
|
| "PROM209"
|
|
42
|
-
| "PROM210"
|
|
55
|
+
| "PROM210"
|
|
56
|
+
| "PROM211"
|
|
57
|
+
| "PROM212"
|
|
58
|
+
| "PROM213"
|
|
59
|
+
| "PROM214"
|
|
60
|
+
| "PROM215"
|
|
61
|
+
| "PROM216"
|
|
62
|
+
| "PROM217"
|
|
63
|
+
| "PROM218"
|
|
64
|
+
| "PROM219"
|
|
65
|
+
| "PROM220"
|
|
66
|
+
| "PROM221"
|
|
67
|
+
| "PROM222"
|
|
68
|
+
| "PROM223"
|
|
69
|
+
| "PROM224";
|
|
43
70
|
|
|
44
71
|
export interface PrometheusIssue {
|
|
45
72
|
code: PrometheusIssueCode;
|
|
@@ -66,7 +93,124 @@ function alertLabels(group: RuleGroupConfig, rule: AlertingRuleConfig): LabelSet
|
|
|
66
93
|
return { ...(group.labels ?? {}), ...(rule.labels ?? {}) };
|
|
67
94
|
}
|
|
68
95
|
|
|
69
|
-
/**
|
|
96
|
+
/** The `$labels.x`, `.Labels.x` and `index $labels "x"` references in a template. */
|
|
97
|
+
function templateLabels(template: string): string[] {
|
|
98
|
+
const out = new Set<string>();
|
|
99
|
+
for (const re of [/\$labels\.([A-Za-z_][A-Za-z0-9_]*)/g, /\.Labels\.([A-Za-z_][A-Za-z0-9_]*)/g, /index\s+\$labels\s+"([^"]+)"/g]) {
|
|
100
|
+
for (const m of template.matchAll(re)) out.add(m[1]);
|
|
101
|
+
}
|
|
102
|
+
return [...out];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `level:metric:operations`: three or more non-empty parts separated by colons. */
|
|
106
|
+
const RECORD_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*(?::[a-zA-Z0-9_]+){2,}$/;
|
|
107
|
+
|
|
108
|
+
/** PROM215, PROM216 and PROM218: what any rule's expression can get wrong. */
|
|
109
|
+
function exprIssues(expr: string, subject: string): PrometheusIssue[] {
|
|
110
|
+
const issues: PrometheusIssue[] = [];
|
|
111
|
+
for (const { fn, name } of nonCounterRates(expr)) {
|
|
112
|
+
issues.push({
|
|
113
|
+
code: "PROM215",
|
|
114
|
+
severity: "warning",
|
|
115
|
+
subject,
|
|
116
|
+
message: `rule ${subject} takes ${fn}() of ${name}, whose name does not end in _total, _count, _sum or _bucket; ${fn}() reads only counters, so use delta() or deriv() for a gauge`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const { call, problem } of histogramProblems(expr)) {
|
|
120
|
+
issues.push({
|
|
121
|
+
code: "PROM216",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
subject,
|
|
124
|
+
message:
|
|
125
|
+
problem === "no-bucket"
|
|
126
|
+
? `rule ${subject} ${call} reads a series without _bucket in its name; a classic histogram's quantile needs its _bucket series`
|
|
127
|
+
: `rule ${subject} ${call} aggregates away the le label; keep le in by (...), or out of without (...)`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const { matcher, problem } of regexProblems(expr)) {
|
|
131
|
+
issues.push({
|
|
132
|
+
code: "PROM218",
|
|
133
|
+
severity: "warning",
|
|
134
|
+
subject,
|
|
135
|
+
message:
|
|
136
|
+
problem === "literal"
|
|
137
|
+
? `rule ${subject} matcher ${matcher} has no regex metacharacters; use = or != instead`
|
|
138
|
+
: `rule ${subject} matcher ${matcher} is anchored; Prometheus anchors every regex, so drop the ^ and $`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return issues;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** PROM211, PROM213, PROM214 and PROM219: alerting rules. */
|
|
145
|
+
function alertIssues(group: RuleGroupConfig, rule: AlertingRuleConfig, subject: string): PrometheusIssue[] {
|
|
146
|
+
const issues: PrometheusIssue[] = [];
|
|
147
|
+
const expr = typeof rule.expr === "string" ? rule.expr : "";
|
|
148
|
+
// An expression that reads no series (`vector(1)`) is a deliberate always-firing
|
|
149
|
+
// alert, a dead man's switch; PROM211 and PROM213 leave it alone.
|
|
150
|
+
const heartbeat = parsePromql(expr) !== undefined && selectors(expr).length === 0;
|
|
151
|
+
|
|
152
|
+
// PROM211: no for, or for: 0s. An expression that already spans a window
|
|
153
|
+
// (every selector read through *_over_time, or the multi-window `and` of two
|
|
154
|
+
// conditions, where the short window does what for would) is left alone. A
|
|
155
|
+
// for that is not a duration is PROM103's.
|
|
156
|
+
const forMs = rule.for === undefined ? 0 : durationMs(str(rule.for));
|
|
157
|
+
if (forMs === 0 && !heartbeat && !readsOnlyOverTime(expr) && !isMultiWindow(expr)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
code: "PROM211",
|
|
160
|
+
severity: "warning",
|
|
161
|
+
subject,
|
|
162
|
+
message: `alert ${subject} has ${rule.for === undefined ? "no for" : `for: ${str(rule.for)}`}, so one evaluation that matches fires it; set for to how long the condition must hold`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// PROM213: no condition
|
|
167
|
+
if (!heartbeat && hasCondition(expr) === false) {
|
|
168
|
+
issues.push({
|
|
169
|
+
code: "PROM213",
|
|
170
|
+
severity: "warning",
|
|
171
|
+
subject,
|
|
172
|
+
message: `alert ${subject} expression has no comparison, so it fires for every series it returns; add a condition such as > 0`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// PROM214: a template reads a label the expression aggregates away. A label
|
|
177
|
+
// the rule or its group sets is left alone.
|
|
178
|
+
const scope = resultLabels(expr);
|
|
179
|
+
if (scope) {
|
|
180
|
+
const fixed = alertLabels(group, rule);
|
|
181
|
+
const reported = new Set<string>();
|
|
182
|
+
for (const [key, value] of [...Object.entries(rule.annotations ?? {}), ...Object.entries(rule.labels ?? {})]) {
|
|
183
|
+
for (const label of templateLabels(str(value))) {
|
|
184
|
+
if (keepsLabel(scope, label) || label in fixed || reported.has(label)) continue;
|
|
185
|
+
reported.add(label);
|
|
186
|
+
issues.push({
|
|
187
|
+
code: "PROM214",
|
|
188
|
+
severity: "warning",
|
|
189
|
+
subject,
|
|
190
|
+
message: `alert ${subject} ${key} reads $labels.${label}, which the expression's by or without drops, so it renders empty`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// PROM219: alertname set by hand
|
|
197
|
+
for (const [where, labels] of [
|
|
198
|
+
["labels", rule.labels],
|
|
199
|
+
["group labels", group.labels],
|
|
200
|
+
] as const) {
|
|
201
|
+
if (labels && Object.prototype.hasOwnProperty.call(labels, "alertname")) {
|
|
202
|
+
issues.push({
|
|
203
|
+
code: "PROM219",
|
|
204
|
+
severity: "warning",
|
|
205
|
+
subject,
|
|
206
|
+
message: `alert ${subject} ${where} set alertname; Prometheus sets alertname to the rule's name and overwrites it`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return issues;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Check a rule file (PROM101-PROM107, PROM211 and PROM213-PROM219). PROM212 is opt-in; see {@link validateRunbookUrls}. */
|
|
70
214
|
export function validateRuleFile(file: RuleFileConfig): PrometheusIssue[] {
|
|
71
215
|
const issues: PrometheusIssue[] = [];
|
|
72
216
|
const groups = Array.isArray(file?.groups) ? file.groups : [];
|
|
@@ -181,7 +325,37 @@ export function validateRuleFile(file: RuleFileConfig): PrometheusIssue[] {
|
|
|
181
325
|
message: `alert ${subject} has no summary or description annotation; a notification carries only its labels`,
|
|
182
326
|
});
|
|
183
327
|
}
|
|
328
|
+
issues.push(...alertIssues(group, rule as AlertingRuleConfig, subject));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// PROM217: recording rule name
|
|
332
|
+
if (recording && !alerting && rule.record.trim() !== "" && !RECORD_NAME.test(rule.record)) {
|
|
333
|
+
issues.push({
|
|
334
|
+
code: "PROM217",
|
|
335
|
+
severity: "warning",
|
|
336
|
+
subject,
|
|
337
|
+
message: `recording rule ${subject} is not named level:metric:operations (e.g. job:http_requests:rate5m)`,
|
|
338
|
+
});
|
|
184
339
|
}
|
|
340
|
+
|
|
341
|
+
if (checked.ok) issues.push(...exprIssues(expr as string, subject));
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return issues;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* PROM212: alerts without a `runbook_url` annotation. Not part of
|
|
349
|
+
* {@link validateRuleFile}, because the check is opt-in: the lint preset
|
|
350
|
+
* `all`, or a `lint.rules` entry for PROM212, turns it on.
|
|
351
|
+
*/
|
|
352
|
+
export function validateRunbookUrls(file: RuleFileConfig): PrometheusIssue[] {
|
|
353
|
+
const issues: PrometheusIssue[] = [];
|
|
354
|
+
for (const group of Array.isArray(file?.groups) ? file.groups : []) {
|
|
355
|
+
(Array.isArray(group?.rules) ? group.rules : []).forEach((rule, i) => {
|
|
356
|
+
if (!isAlertingRuleConfig(rule) || rule.annotations?.runbook_url) return;
|
|
357
|
+
const subject = ruleLabel(group, i, rule);
|
|
358
|
+
issues.push({ code: "PROM212", severity: "warning", subject, message: `alert ${subject} has no runbook_url annotation` });
|
|
185
359
|
});
|
|
186
360
|
}
|
|
187
361
|
return issues;
|
|
@@ -223,7 +397,83 @@ function visitRoutes(root: RouteConfig | undefined): RouteVisit[] {
|
|
|
223
397
|
|
|
224
398
|
const AM_DURATIONS = ["group_wait", "group_interval", "repeat_interval"] as const;
|
|
225
399
|
|
|
226
|
-
/**
|
|
400
|
+
/** Alertmanager's defaults for a root route that sets neither (config/config.go). */
|
|
401
|
+
const DEFAULT_GROUP_INTERVAL_MS = 5 * 60_000;
|
|
402
|
+
const DEFAULT_REPEAT_INTERVAL_MS = 4 * 3_600_000;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* PROM223: routes whose repeat_interval, as set or inherited, is shorter
|
|
406
|
+
* than their group_interval. Alertmanager only resends when a group is
|
|
407
|
+
* flushed, every group_interval, so the shorter repeat_interval never takes
|
|
408
|
+
* effect. Reported on the route that sets one of the two.
|
|
409
|
+
*/
|
|
410
|
+
function repeatIntervalIssues(root: RouteConfig | undefined): PrometheusIssue[] {
|
|
411
|
+
const issues: PrometheusIssue[] = [];
|
|
412
|
+
const walk = (route: RouteConfig, path: string, groupMs: number, repeatMs: number) => {
|
|
413
|
+
const ownGroup = route.group_interval !== undefined ? durationMs(str(route.group_interval)) : groupMs;
|
|
414
|
+
const ownRepeat = route.repeat_interval !== undefined ? durationMs(str(route.repeat_interval)) : repeatMs;
|
|
415
|
+
if (ownGroup === undefined || ownRepeat === undefined) return; // PROM208
|
|
416
|
+
if ((route.group_interval !== undefined || route.repeat_interval !== undefined) && ownRepeat < ownGroup) {
|
|
417
|
+
issues.push({
|
|
418
|
+
code: "PROM223",
|
|
419
|
+
severity: "warning",
|
|
420
|
+
subject: path,
|
|
421
|
+
message: `${path} repeat_interval (${formatDuration(ownRepeat)}) is shorter than its group_interval (${formatDuration(ownGroup)}); notifications repeat no faster than group_interval`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
(Array.isArray(route.routes) ? route.routes : []).forEach((child, i) => walk(child, `${path}.routes[${i}]`, ownGroup, ownRepeat));
|
|
425
|
+
};
|
|
426
|
+
if (root && typeof root === "object") walk(root, "route", DEFAULT_GROUP_INTERVAL_MS, DEFAULT_REPEAT_INTERVAL_MS);
|
|
427
|
+
return issues;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function parsedList(entries: unknown): Matcher[] | undefined {
|
|
431
|
+
const out: Matcher[] = [];
|
|
432
|
+
for (const m of Array.isArray(entries) ? entries : []) {
|
|
433
|
+
const parsed = parseMatchers(str(m));
|
|
434
|
+
if (!parsed.ok) return undefined; // PROM206
|
|
435
|
+
out.push(...parsed.matchers);
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** A regex's alternatives when every one is a plain string, e.g. `a|b` gives `["a", "b"]`. */
|
|
441
|
+
function literalAlternatives(regex: string): string[] | undefined {
|
|
442
|
+
const parts = regex.split("|");
|
|
443
|
+
return parts.every((p) => !/[.+*?()[\]{}\\^$]/.test(p)) ? parts : undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Whether some label set matches every matcher. A label whose matchers
|
|
448
|
+
* name values (`=`, or a regex of plain alternatives) is tried with those
|
|
449
|
+
* values; one with only negative or open-ended matchers is taken to have a
|
|
450
|
+
* value that passes.
|
|
451
|
+
*/
|
|
452
|
+
function satisfiable(matchers: Matcher[]): boolean {
|
|
453
|
+
const byLabel = new Map<string, Matcher[]>();
|
|
454
|
+
for (const m of matchers) byLabel.set(m.name, [...(byLabel.get(m.name) ?? []), m]);
|
|
455
|
+
for (const [name, ms] of byLabel) {
|
|
456
|
+
const candidates = new Set<string>();
|
|
457
|
+
let bounded = false;
|
|
458
|
+
for (const m of ms) {
|
|
459
|
+
if (m.op === "=") {
|
|
460
|
+
candidates.add(m.value);
|
|
461
|
+
bounded = true;
|
|
462
|
+
} else if (m.op === "=~") {
|
|
463
|
+
const alts = literalAlternatives(m.value);
|
|
464
|
+
if (alts) {
|
|
465
|
+
alts.forEach((a) => candidates.add(a));
|
|
466
|
+
bounded = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!bounded) continue;
|
|
471
|
+
if (![...candidates].some((v) => ms.every((m) => matcherMatches(m, { [name]: v })))) return false;
|
|
472
|
+
}
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Check an `alertmanager.yml` on its own (PROM201, PROM203-PROM210, PROM220-PROM224). */
|
|
227
477
|
export function validateAlertmanagerConfig(config: AlertmanagerConfig): PrometheusIssue[] {
|
|
228
478
|
const issues: PrometheusIssue[] = [];
|
|
229
479
|
const receivers = Array.isArray(config?.receivers) ? config.receivers : [];
|
|
@@ -309,8 +559,26 @@ export function validateAlertmanagerConfig(config: AlertmanagerConfig): Promethe
|
|
|
309
559
|
}
|
|
310
560
|
});
|
|
311
561
|
|
|
312
|
-
//
|
|
562
|
+
// PROM223: repeat_interval under group_interval
|
|
563
|
+
issues.push(...repeatIntervalIssues(root));
|
|
564
|
+
|
|
565
|
+
// PROM224: an inhibit rule an alert can be both source and target of, with no equal
|
|
566
|
+
(Array.isArray(config?.inhibit_rules) ? config.inhibit_rules : []).forEach((rule, i) => {
|
|
567
|
+
if (Array.isArray(rule?.equal) && rule.equal.length > 0) return;
|
|
568
|
+
const source = parsedList(rule?.source_matchers);
|
|
569
|
+
const target = parsedList(rule?.target_matchers);
|
|
570
|
+
if (!source || !target || !satisfiable([...source, ...target])) return;
|
|
571
|
+
issues.push({
|
|
572
|
+
code: "PROM224",
|
|
573
|
+
severity: "warning",
|
|
574
|
+
subject: `inhibit_rules[${i}]`,
|
|
575
|
+
message: `inhibit_rules[${i}]: one alert can match both source_matchers and target_matchers, and there is no equal, so any such alert firing mutes every other; list the labels the two must share under equal`,
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// PROM208, PROM210: global settings; PROM220: global TLS
|
|
313
580
|
issues.push(...validateGlobalSettings(global));
|
|
581
|
+
issues.push(...validateGlobalSecurity(global));
|
|
314
582
|
|
|
315
583
|
// PROM207: unused receivers; PROM208-PROM210: integrations (./validate-integrations.ts)
|
|
316
584
|
for (const r of receivers) {
|
|
@@ -319,6 +587,7 @@ export function validateAlertmanagerConfig(config: AlertmanagerConfig): Promethe
|
|
|
319
587
|
issues.push({ code: "PROM207", severity: "warning", subject: name, message: `receiver "${name}" is declared but no route sends to it` });
|
|
320
588
|
}
|
|
321
589
|
issues.push(...validateReceiverIntegrations(r, global));
|
|
590
|
+
issues.push(...validateReceiverSecurity(r, global));
|
|
322
591
|
// A receiver with no integrations at all is valid: it is how Alertmanager drops alerts.
|
|
323
592
|
}
|
|
324
593
|
|
|
@@ -77,7 +77,7 @@ function isHostPort(v: unknown): boolean {
|
|
|
77
77
|
return m !== null && m[1] !== "";
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const SLACK_APP_URL = "https://slack.com/api/chat.postMessage";
|
|
80
|
+
export const SLACK_APP_URL = "https://slack.com/api/chat.postMessage";
|
|
81
81
|
|
|
82
82
|
class Reporter {
|
|
83
83
|
readonly issues: PrometheusIssue[] = [];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Alertmanager accepts that weaken how it talks to a receiver
|
|
3
|
+
* (PROM220-PROM222): certificate checks turned off, SMTP credentials sent
|
|
4
|
+
* without TLS, and credentials sent to an `http://` URL. `amtool
|
|
5
|
+
* check-config` passes all three.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AlertmanagerGlobalConfig, ReceiverConfig } from "./model";
|
|
9
|
+
import type { PrometheusIssue } from "./validate-config";
|
|
10
|
+
|
|
11
|
+
type Obj = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
function asObj(v: unknown): Obj {
|
|
14
|
+
return v && typeof v === "object" && !Array.isArray(v) ? (v as Obj) : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asList(v: unknown): unknown[] {
|
|
18
|
+
return Array.isArray(v) ? v : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function has(v: unknown): boolean {
|
|
22
|
+
return v !== undefined && v !== null && v !== "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Every `tls_config` under `value` with `insecure_skip_verify: true`, by path. */
|
|
26
|
+
function insecureTls(value: unknown, path: string): string[] {
|
|
27
|
+
if (Array.isArray(value)) return value.flatMap((v, i) => insecureTls(v, `${path}[${i}]`));
|
|
28
|
+
if (!value || typeof value !== "object") return [];
|
|
29
|
+
const out: string[] = [];
|
|
30
|
+
for (const [key, v] of Object.entries(value as Obj)) {
|
|
31
|
+
const at = path ? `${path}.${key}` : key;
|
|
32
|
+
if (/tls_config$/.test(key) && asObj(v).insecure_skip_verify === true) out.push(at);
|
|
33
|
+
out.push(...insecureTls(v, at));
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The fields that carry a destination URL in an integration entry. */
|
|
39
|
+
const URL_FIELDS = ["url", "api_url", "webhook_url"];
|
|
40
|
+
|
|
41
|
+
/** Where an integration takes its URL and credential from `global:` when the entry leaves them out. */
|
|
42
|
+
const GLOBAL_FALLBACK: Record<string, { url: string; credentials: string[] }> = {
|
|
43
|
+
pagerduty_configs: { url: "pagerduty_url", credentials: [] },
|
|
44
|
+
opsgenie_configs: { url: "opsgenie_api_url", credentials: ["opsgenie_api_key", "opsgenie_api_key_file"] },
|
|
45
|
+
wechat_configs: { url: "wechat_api_url", credentials: ["wechat_api_secret", "wechat_api_secret_file"] },
|
|
46
|
+
victorops_configs: { url: "victorops_api_url", credentials: ["victorops_api_key", "victorops_api_key_file"] },
|
|
47
|
+
telegram_configs: { url: "telegram_api_url", credentials: ["telegram_bot_token", "telegram_bot_token_file"] },
|
|
48
|
+
webex_configs: { url: "webex_api_url", credentials: [] },
|
|
49
|
+
rocketchat_configs: { url: "rocketchat_api_url", credentials: ["rocketchat_token", "rocketchat_token_file", "rocketchat_token_id", "rocketchat_token_id_file"] },
|
|
50
|
+
jira_configs: { url: "jira_api_url", credentials: [] },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Entry fields that hold a credential the integration sends with each request. */
|
|
54
|
+
const CREDENTIAL_FIELDS = ["api_key", "service_key", "token", "bot_token", "api_secret", "user_key", "token_id", "alert_source_token", "app_token"].flatMap((f) => [
|
|
55
|
+
f,
|
|
56
|
+
`${f}_file`,
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** The credentials an HTTP client config attaches, by name. */
|
|
60
|
+
function httpCredentials(http: Obj): string[] {
|
|
61
|
+
const out: string[] = [];
|
|
62
|
+
if (Object.keys(asObj(http.basic_auth)).length > 0) out.push("basic_auth");
|
|
63
|
+
if (Object.keys(asObj(http.authorization)).length > 0) out.push("authorization");
|
|
64
|
+
if (Object.keys(asObj(http.oauth2)).length > 0) out.push("oauth2");
|
|
65
|
+
if (has(http.bearer_token) || has(http.bearer_token_file)) out.push("bearer_token");
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** PROM220-PROM222 for one receiver. */
|
|
70
|
+
export function validateReceiverSecurity(receiver: ReceiverConfig, global: AlertmanagerGlobalConfig): PrometheusIssue[] {
|
|
71
|
+
const issues: PrometheusIssue[] = [];
|
|
72
|
+
const name = String(receiver?.name ?? "");
|
|
73
|
+
const g = asObj(global);
|
|
74
|
+
const rec = receiver as unknown as Obj;
|
|
75
|
+
|
|
76
|
+
// PROM220
|
|
77
|
+
for (const at of insecureTls(rec, "")) {
|
|
78
|
+
issues.push({
|
|
79
|
+
code: "PROM220",
|
|
80
|
+
severity: "warning",
|
|
81
|
+
subject: name,
|
|
82
|
+
message: `receiver "${name}" ${at}.insecure_skip_verify is true, so Alertmanager accepts any certificate the endpoint presents`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const [key, list] of Object.entries(rec)) {
|
|
87
|
+
if (!key.endsWith("_configs")) continue;
|
|
88
|
+
asList(list).forEach((raw, i) => {
|
|
89
|
+
const entry = asObj(raw);
|
|
90
|
+
const at = `receiver "${name}" ${key}[${i}]`;
|
|
91
|
+
|
|
92
|
+
// PROM221
|
|
93
|
+
if (key === "email_configs") {
|
|
94
|
+
const requireTls = entry.require_tls ?? g.smtp_require_tls ?? true;
|
|
95
|
+
const auth = ["auth_password", "auth_password_file", "auth_secret", "auth_secret_file"].some((f) => has(entry[f]) || has(g[`smtp_${f}`]));
|
|
96
|
+
if (requireTls === false && auth && entry.force_implicit_tls !== true && g.smtp_force_implicit_tls !== true) {
|
|
97
|
+
issues.push({
|
|
98
|
+
code: "PROM221",
|
|
99
|
+
severity: "warning",
|
|
100
|
+
subject: name,
|
|
101
|
+
message: `${at} sends SMTP credentials with require_tls false${entry.require_tls === undefined ? " (from global.smtp_require_tls)" : ""}, so they can cross the network in clear text`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// PROM222
|
|
108
|
+
const fallback = GLOBAL_FALLBACK[key];
|
|
109
|
+
const urls = URL_FIELDS.filter((f) => typeof entry[f] === "string").map((f) => [f, entry[f] as string] as const);
|
|
110
|
+
if (urls.length === 0 && fallback && typeof g[fallback.url] === "string") urls.push([`global.${fallback.url}`, g[fallback.url] as string]);
|
|
111
|
+
for (const [field, url] of urls) {
|
|
112
|
+
if (!/^http:\/\//i.test(url)) continue;
|
|
113
|
+
const http = entry.http_config !== undefined ? asObj(entry.http_config) : asObj(g.http_config);
|
|
114
|
+
const credentials = [
|
|
115
|
+
...(/^http:\/\/[^/@]*@/i.test(url) ? ["user info in the URL"] : []),
|
|
116
|
+
...httpCredentials(http),
|
|
117
|
+
...CREDENTIAL_FIELDS.filter((f) => has(entry[f])),
|
|
118
|
+
// victorops' routing_key is part of the URL path, not a secret; pagerduty's is the integration key.
|
|
119
|
+
...(key === "pagerduty_configs" ? ["routing_key", "routing_key_file"].filter((f) => has(entry[f])) : []),
|
|
120
|
+
...(fallback?.credentials ?? []).filter((f) => has(g[f]) && !CREDENTIAL_FIELDS.some((c) => has(entry[c]))).map((f) => `global.${f}`),
|
|
121
|
+
];
|
|
122
|
+
if (credentials.length === 0) continue;
|
|
123
|
+
issues.push({
|
|
124
|
+
code: "PROM222",
|
|
125
|
+
severity: "warning",
|
|
126
|
+
subject: name,
|
|
127
|
+
message: `${at} ${field} is http://, and the request carries ${credentials.join(", ")}; use https://`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return issues;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** PROM220 for `global:`'s own TLS settings, which every receiver without its own inherits. */
|
|
136
|
+
export function validateGlobalSecurity(global: AlertmanagerGlobalConfig): PrometheusIssue[] {
|
|
137
|
+
return insecureTls(asObj(global), "global").map((at) => ({
|
|
138
|
+
code: "PROM220" as const,
|
|
139
|
+
severity: "warning" as const,
|
|
140
|
+
subject: "global",
|
|
141
|
+
message: `${at}.insecure_skip_verify is true, so Alertmanager accepts any certificate a receiver's endpoint presents`,
|
|
142
|
+
}));
|
|
143
|
+
}
|