Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(agent/compositeRules): exclude rules that does not have trigger alert #308

Merged
merged 1 commit into from
Sep 23, 2024
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
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