Skip to content

Commit

Permalink
fix(agent/compositeRules): exclude rules that does not have trigger a…
Browse files Browse the repository at this point in the history
…lert (#308)
  • Loading branch information
PierreDemailly authored Sep 23, 2024
1 parent d9b4d09 commit 838c1f7
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 168 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,6 @@ dist
src/**/temp
docs/.vitepress/dist
docs/.vitepress/cache

# Ignore all local SQLite files
*.sqlite3*
16 changes: 9 additions & 7 deletions src/agent/src/compositeRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ export function handleCompositeRules(logger: Logger) {
}

for (const compositeRule of compositeRules) {
const ruleIdsObj = getDB().prepare(
`SELECT id FROM rules WHERE name IN (${compositeRule.rules.map(() => "?").join(",")})`
).all(compositeRule.rules) as { id: number }[];
const ruleIds = ruleIdsObj.map(({ id }) => id);
const rulesObj = getDB().prepare(
`SELECT id, name FROM rules WHERE name IN (${compositeRule.rules.map(() => "?").join(",")})`
).all(compositeRule.rules) as { id: number, name: string }[];
const ruleIds = rulesObj.map(({ id }) => id);
const subtractedInterval = utils.cron.durationOrCronToDate(compositeRule.interval, "subtract").valueOf();
const { count } = getDB()
// eslint-disable-next-line max-len
Expand All @@ -76,10 +76,11 @@ export function handleCompositeRules(logger: Logger) {
subtractedInterval,
...ruleIds
) as { count: number };
const processedRulesIdsObj = getDB()
const processedRules = getDB()
.prepare(`SELECT ruleId FROM alerts WHERE createdAt >= ? AND ruleId IN (${ruleIds.map(() => "?").join(",")})`)
.all(subtractedInterval, ...ruleIds) as { ruleId: number }[];
const processedRulesIds = [...new Set(processedRulesIdsObj.flatMap(({ ruleId }) => ruleIds.filter((id) => id === ruleId)))];
const processedRulesIds = processedRules.map(({ ruleId }) => ruleId);
const processedRulesNames = rulesObj.filter(({ id }) => processedRulesIds.includes(id)).map(({ name }) => name);

if (count < compositeRule.notifCount) {
logger.info(`[${compositeRule.name}](alertsCount:${count}|notifCount:${compositeRule.notifCount})`);
Expand Down Expand Up @@ -117,7 +118,8 @@ export function handleCompositeRules(logger: Logger) {
compositeRule.notifiers.map((notifierName) => {
return {
notifierConfig: notifiers[notifierName],
compositeRuleName: compositeRule.name
compositeRuleName: compositeRule.name,
ruleNames: processedRulesNames
};
})
);
Expand Down
5 changes: 3 additions & 2 deletions src/agent/src/notifiers/compositeRules.notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const kCompositeRuleSeverity = "critical";

export interface CompositeRuleAlert extends Alert {
compositeRuleName: string;
ruleNames: string[];
}

export class CompositeRuleNotifier extends Notifier<CompositeRuleAlert> {
Expand Down Expand Up @@ -52,7 +53,7 @@ export class CompositeRuleNotifier extends Notifier<CompositeRuleAlert> {
}

#compositeRuleAlertData(alert: CompositeRuleAlert) {
const { compositeRuleName } = alert;
const { compositeRuleName, ruleNames } = alert;

const compositeRule = this.config.compositeRules!.find((compositeRule) => compositeRule.name === compositeRuleName)!;
const rulesLabels = Object.create(null);
Expand All @@ -73,7 +74,7 @@ export class CompositeRuleNotifier extends Notifier<CompositeRuleAlert> {
severity: kCompositeRuleSeverity,
compositeRuleName,
label: rulesLabels,
rules: compositeRule.rules.join(", ")
rules: ruleNames.join(", ")
};
}
}
160 changes: 1 addition & 159 deletions src/agent/test/FT/compositeRules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,12 @@ import isCI from "is-ci";

// Import Internal Dependencies
import { getDB, initDB } from "../../src/database";
import { MockLogger } from "./helpers";
import { createRuleAlert, MockLogger, resetRuteMuteUntil, ruleMuteUntilTimestamp } from "./helpers";
import { handleCompositeRules } from "../../src/compositeRules";
import { Rule } from "../../src/rules";

// CONSTANTS
const kCompositeRulesConfigLocation = path.join(__dirname, "/fixtures/composite-rules/sigyn.config.json");
const kUntriggeredCompositeRulesConfigLocation = path.join(
__dirname,
"/fixtures/composite-rules-no-mute-untriggered/sigyn.config.json"
);
const kSeverityFilterCompositeRulesConfigLocation = path.join(
__dirname,
"/fixtures/composite-rules-sev-filters/sigyn.config.json"
);
const kLogger = new MockLogger();
const kMockAgent = new MockAgent();
const kGlobalDispatcher = getGlobalDispatcher();
Expand Down Expand Up @@ -178,153 +170,3 @@ describe("Composite Rules", { concurrency: 1 }, () => {
assert.ok(ruleMuteUntilTimestamp(rules[2]) > Date.now());
});
});

describe("Composite Rules with muteUntriggered falsy", { concurrency: 1 }, () => {
let config: SigynInitializedConfig;
let rules: any;

before(async() => {
fs.mkdirSync(".temp", { recursive: true });

initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" });

process.env.GRAFANA_API_TOKEN = "toto";
setGlobalDispatcher(kMockAgent);

const pool = kMockAgent.get("https://discord.com");
pool.intercept({
method: "POST",
path: () => true
}).reply(200);

initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" });

config = await initConfig(kUntriggeredCompositeRulesConfigLocation);
rules = config.rules.map((ruleConfig) => {
const rule = new Rule(ruleConfig, { logger: kLogger });
rule.init();

return rule;
});
});

beforeEach(() => {
getDB().prepare("DELETE FROM alerts").run();
});

after(() => {
setGlobalDispatcher(kGlobalDispatcher);
});

it("should not mute rules that have not triggered alerts when muteUntrigged is false", async() => {
resetRuteMuteUntil(rules[0]);
resetRuteMuteUntil(rules[1]);
resetRuteMuteUntil(rules[2]);

getDB().prepare("DELETE FROM compositeRuleAlerts").run();

assert.equal(ruleMuteUntilTimestamp(rules[0]), 0);
assert.equal(ruleMuteUntilTimestamp(rules[1]), 0);
assert.equal(ruleMuteUntilTimestamp(rules[2]), 0);

createRuleAlert(rules[0], 5);
createRuleAlert(rules[1], 5);
// DO NOT SEND ALERTS FOR RULE 2
// createRuleAlert(rules[2], 2);

handleCompositeRules(kLogger);
await setTimeout(kTimeout);

assert.ok(ruleMuteUntilTimestamp(rules[0]) > Date.now());
assert.ok(ruleMuteUntilTimestamp(rules[1]) > Date.now());
assert.equal(ruleMuteUntilTimestamp(rules[2]), 0);
});
});

describe("Composite Rules with severity filters", { concurrency: 1 }, () => {
let config: SigynInitializedConfig;
let rules: any;

before(async() => {
fs.mkdirSync(".temp", { recursive: true });

initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" });

process.env.GRAFANA_API_TOKEN = "toto";
setGlobalDispatcher(kMockAgent);

const pool = kMockAgent.get("https://discord.com");
pool.intercept({
method: "POST",
path: () => true
}).reply(200);

initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" });

config = await initConfig(kSeverityFilterCompositeRulesConfigLocation);
rules = config.rules.map((ruleConfig) => {
const rule = new Rule(ruleConfig, { logger: kLogger });
rule.init();

return rule;
});
});

beforeEach(() => {
getDB().prepare("DELETE FROM alerts").run();
});

after(() => {
setGlobalDispatcher(kGlobalDispatcher);
});

it("should not mute rules that have not triggered alerts when muteUntrigged is false", async() => {
resetRuteMuteUntil(rules[0]);
resetRuteMuteUntil(rules[1]);
resetRuteMuteUntil(rules[2]);
resetRuteMuteUntil(rules[3]);

getDB().prepare("DELETE FROM compositeRuleAlerts").run();

assert.equal(ruleMuteUntilTimestamp(rules[0]), 0);
assert.equal(ruleMuteUntilTimestamp(rules[1]), 0);
assert.equal(ruleMuteUntilTimestamp(rules[2]), 0);
assert.equal(ruleMuteUntilTimestamp(rules[3]), 0);

createRuleAlert(rules[0], 5);
createRuleAlert(rules[1], 5);
createRuleAlert(rules[2], 5);
createRuleAlert(rules[3], 5);

handleCompositeRules(kLogger);
await setTimeout(kTimeout);

assert.ok(ruleMuteUntilTimestamp(rules[0]) > Date.now());
assert.equal(ruleMuteUntilTimestamp(rules[1]), 0);
assert.ok(ruleMuteUntilTimestamp(rules[2]) > Date.now());
assert.ok(ruleMuteUntilTimestamp(rules[3]) > Date.now());
});
});

function createRuleAlert(rule: Rule, times: number) {
let i = 0;
while (i++ < times) {
getDB()
.prepare("INSERT INTO alerts (ruleId, createdAt) VALUES (?, ?)")
.run(rule.getRuleFromDatabase().id, Date.now());
}
}

function ruleMuteUntilTimestamp(rule: Rule): number {
const { muteUntil } = getDB()
.prepare("SELECT muteUntil FROM rules WHERE name = ?")
.get(rule.config.name) as { muteUntil: number };

return muteUntil;
}

function resetRuteMuteUntil(rule: Rule): void {
getDB()
.prepare("UPDATE rules SET muteUntil = ? WHERE name = ?")
.run(0, rule.config.name);
}
72 changes: 72 additions & 0 deletions src/agent/test/FT/compositeRulesLocalNotifier.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Import Node.js Dependencies
import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import { before, beforeEach, describe, it } from "node:test";
import { setTimeout } from "node:timers/promises";

// Import Third-party Dependencies
import { SigynInitializedConfig, initConfig } from "@sigyn/config";
import isCI from "is-ci";

// Import Internal Dependencies
import { getDB, initDB } from "../../src/database";
import { createRuleAlert, MockLogger } from "./helpers";
import { handleCompositeRules } from "../../src/compositeRules";
import { Rule } from "../../src/rules";
import { TestingNotifier } from "./mocks/sigyn-test-notifier.js";

// CONSTANTS
const kCompositeRulesConfigLocation = path.join(__dirname, "/fixtures/composite-rules-local/sigyn.config.json");
const kLogger = new MockLogger();
// time to wait for the task to be fully executed (alert sent)
const kTimeout = isCI ? 350 : 200;
const kTestingNotifier = TestingNotifier.getInstance();

describe("Composite Rules with Local Notifier", { concurrency: 1 }, () => {
let config: SigynInitializedConfig;
let rules: any;

before(async() => {
fs.mkdirSync(".temp", { recursive: true });

initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" });

process.env.GRAFANA_API_TOKEN = "toto";

initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" });

config = await initConfig(kCompositeRulesConfigLocation);
rules = config.rules.map((ruleConfig) => {
const rule = new Rule(ruleConfig, { logger: kLogger });
rule.init();

return rule;
});

kTestingNotifier.clear();
});

beforeEach(() => {
getDB().prepare("DELETE FROM alerts").run();
});

it("should includes rules that have triggered alert only", async() => {
getDB().prepare("DELETE FROM compositeRuleAlerts").run();
createRuleAlert(rules[0], 5);
createRuleAlert(rules[1], 5);

handleCompositeRules(kLogger);
await setTimeout(kTimeout);

const [notif] = kTestingNotifier.notifArguments;
const { rules: notifedRules } = notif.data;

assert.ok(notifedRules.includes(rules[0].getRuleFromDatabase().name), "Rule 0 has trigger alert so it should be included");
assert.ok(notifedRules.includes(rules[1].getRuleFromDatabase().name), "Rule 1 has trigger alert so it should be included");
assert.ok(
notifedRules.includes(rules[2].getRuleFromDatabase().name) === false,
"Rule 2 has not trigger alert so it should not be included"
);
});
});
Loading

0 comments on commit 838c1f7

Please sign in to comment.