Skip to content

Commit 664992e

Browse files
authored
[core][feat] Maintain account score in metadata (#2030)
* [core][feat] Maintain account score in metadata * my IDE tries to be clever
1 parent 3a1c4d4 commit 664992e

File tree

13 files changed

+125
-82
lines changed

13 files changed

+125
-82
lines changed

fixcore/fixcore/report/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
from abc import ABC, abstractmethod
5+
from collections import defaultdict
56
from enum import Enum
67
from functools import reduce, total_ordering
78
from typing import List, Optional, Dict, ClassVar, AsyncIterator, cast, Set, Tuple, Any
@@ -27,6 +28,7 @@
2728

2829

2930
_ReportSeverityPriority: Dict[str, int] = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
31+
_ReportSeverityScore: Dict[str, int] = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
3032

3133

3234
@total_ordering
@@ -38,12 +40,17 @@ class ReportSeverity(Enum):
3840
high = "high"
3941
critical = "critical"
4042

43+
@property
4144
def prio(self) -> int:
4245
return _ReportSeverityPriority[self.value]
4346

47+
@property
48+
def score(self) -> int:
49+
return _ReportSeverityScore[self.value]
50+
4451
def __lt__(self, other: Any) -> bool:
4552
if isinstance(other, ReportSeverity):
46-
return self.prio() < other.prio()
53+
return self.prio < other.prio
4754
return NotImplemented
4855

4956
@staticmethod
@@ -359,6 +366,24 @@ def visit_check_collection(collection: CheckCollectionResult) -> None:
359366
visit_check_collection(self)
360367
return result
361368

369+
def score_for(self, account: str) -> int:
370+
failing: Dict[ReportSeverity, int] = defaultdict(int)
371+
available: Dict[ReportSeverity, int] = defaultdict(int)
372+
373+
def available_failing(check: CheckCollectionResult) -> None:
374+
for result in check.checks:
375+
available[result.check.severity] += 1
376+
failing[result.check.severity] += (
377+
1 if result.number_of_resources_failing_by_account.get(account, 0) else 0
378+
)
379+
for child in check.children:
380+
available_failing(child)
381+
382+
available_failing(self) # walk the benchmark hierarchy
383+
missing = sum(severity.score * count for severity, count in failing.items())
384+
total = sum(severity.score * count for severity, count in available.items())
385+
return int((max(0, total - missing) * 100) // total) if total > 0 else 100
386+
362387

363388
class Inspector(ABC):
364389
"""

fixcore/fixcore/report/benchmark_renderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def render_benchmark_summary(benchmark: CheckCollectionResult, account: str) ->
4747
def render_result_list(name: str, icon: str, checks: List[CheckResult]) -> str:
4848
if checks:
4949
lr = f"## {name} \n\n"
50-
for check in sorted(checks, key=lambda x: -x.check.severity.prio()):
50+
for check in sorted(checks, key=lambda x: -x.check.severity.prio):
5151
lr += f"- {icon} {check.check.severity.name}: {check.check.title}\n"
5252
return lr
5353
else:

fixcore/fixcore/report/inspector_service.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def includes_severity(self, severity: ReportSeverity) -> bool:
6262
if self.severity is None:
6363
return True
6464
else:
65-
return self.severity.prio() <= severity.prio()
65+
return self.severity.prio <= severity.prio
6666

6767
def override_values(self) -> Optional[Json]:
6868
return self.config.override_values
@@ -241,8 +241,10 @@ async def perform_benchmarks(
241241
report_run_id: Optional[str] = None,
242242
) -> Dict[str, BenchmarkResult]:
243243
config = await self.report_config()
244-
context = CheckContext(accounts=accounts, severity=severity, only_failed=only_failing, config=config)
245244
benchmarks = await self.__benchmarks(benchmark_names)
245+
clouds = {c for b in benchmarks.values() for c in b.clouds or []}
246+
cloud_accounts = accounts or await self.__list_accounts(list(clouds), graph)
247+
context = CheckContext(accounts=cloud_accounts, severity=severity, only_failed=only_failing, config=config)
246248
# collect all checks
247249
check_ids = {check for b in benchmarks.values() for check in b.nested_checks() if config.check_allowed(check)}
248250
checks = await self.list_checks(check_ids=list(check_ids), context=context)
@@ -256,9 +258,22 @@ async def perform_benchmarks(
256258
model = await self.model_handler.load_model(graph)
257259
# In case no run_id is provided, we invent a report run id here.
258260
run_id = report_run_id or uuid_str()
259-
await self.db_access.get_graph_db(graph).update_security_section(
260-
run_id, self.__benchmarks_to_security_iterator(result), model, accounts
261-
)
261+
db = self.db_access.get_graph_db(graph)
262+
await db.update_security_section(run_id, self.__benchmarks_to_security_iterator(result), model, accounts)
263+
# store the security score of an account
264+
if account_ids := context.accounts:
265+
async with await db.search_list( # lookup node ids for all given accounts
266+
QueryModel(Query.by("account", P("reported.id").is_in(account_ids)), model)
267+
) as crsr:
268+
async for acc in crsr:
269+
if (node_id := value_in_path(acc, NodePath.node_id)) and (
270+
acc_id := value_in_path(acc, NodePath.reported_id)
271+
):
272+
bench_score = {br.id: br.score_for(acc_id) for br in result.values()}
273+
account_score = sum(bench_score.values()) / len(bench_score) if bench_score else 100
274+
patch = {"score": account_score, "benchmark": bench_score}
275+
async for _ in db.update_nodes_metadata(model, patch, [node_id]):
276+
pass
262277
return result
263278

264279
async def perform_checks(
@@ -303,7 +318,7 @@ async def perform_checks(
303318
)
304319

305320
if context.accounts is None:
306-
context.accounts = await self.__list_accounts(benchmark, graph)
321+
context.accounts = await self.__list_accounts(benchmark.clouds, graph)
307322

308323
checks_to_perform = await self.list_checks(check_ids=benchmark.nested_checks(), context=context)
309324
check_by_id = {c.id: c for c in checks_to_perform}
@@ -449,12 +464,12 @@ async def __perform_check(
449464
resources_by_account[account_id].append(bend(ReportResourceData, resource))
450465
return resources_by_account
451466

452-
async def __list_accounts(self, benchmark: Benchmark, graph: GraphName) -> List[str]:
467+
async def __list_accounts(self, clouds: Optional[List[str]], graph: GraphName) -> List[str]:
453468
model = await self.model_handler.load_model(graph)
454469
gdb = self.db_access.get_graph_db(graph)
455470
query = Query.by("account")
456-
if benchmark.clouds:
457-
query = query.combine(Query.by(P.single("ancestors.cloud.reported.id").is_in(benchmark.clouds)))
471+
if clouds:
472+
query = query.combine(Query.by(P.single("ancestors.cloud.reported.id").is_in(clouds)))
458473
async with await gdb.search_list(QueryModel(query, model)) as crs:
459474
ids = [value_in_path(a, NodePath.reported_id) async for a in crs]
460475
return [aid for aid in ids if aid is not None]

fixcore/tests/fixcore/cli/cli_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,14 @@ def test_parse_predecessor_successor_ancestor_descendant_args() -> None:
172172

173173
@pytest.mark.asyncio
174174
async def test_create_query_parts(cli: CLI) -> None:
175-
commands = await cli.evaluate_cli_command('search some_int==0 | search identifier=~"9_" | descendants')
175+
commands = await cli.evaluate_cli_command('search some_int==0 | search id=~"9_" | descendants')
176176
sort = "sort reported.kind asc, reported.name asc, reported.id asc"
177177
assert len(commands) == 1
178178
assert len(commands[0].commands) == 2 # list command is added automagically
179179
assert commands[0].commands[0].name == "execute_search"
180180
assert (
181181
commands[0].executable_commands[0].arg
182-
== f"'(reported.some_int == 0 and reported.identifier =~ \"9_\") {sort} -default[1:]-> all {sort}'"
182+
== f"'(reported.some_int == 0 and reported.id =~ \"9_\") {sort} -default[1:]-> all {sort}'"
183183
)
184184
commands = await cli.evaluate_cli_command("search some_int==0 | descendants")
185185
assert "-default[1:]->" in commands[0].executable_commands[0].arg # type: ignore

fixcore/tests/fixcore/cli/command_test.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,9 @@ async def test_descendants(cli: CLI) -> None:
141141

142142
@pytest.mark.asyncio
143143
async def test_search_source(cli: CLIService) -> None:
144-
result = await cli.execute_cli_command('search is("foo") and some_int==0 --> identifier=~"9_"', list_sink)
144+
result = await cli.execute_cli_command('search is("foo") and some_int==0 --> id=~"9_"', list_sink)
145145
assert len(result[0]) == 10
146-
await cli.dependencies.template_expander.put_template(
147-
Template("test", 'is(foo) and some_int==0 --> identifier=~"{{fid}}"')
148-
)
146+
await cli.dependencies.template_expander.put_template(Template("test", 'is(foo) and some_int==0 --> id=~"{{fid}}"'))
149147
result2 = await cli.execute_cli_command('search expand(test, fid="9_")', list_sink)
150148
assert len(result2[0]) == 10
151149

@@ -502,7 +500,7 @@ async def test_kinds_command(cli: CLI, foo_model: Model) -> None:
502500
"properties": {
503501
"age": "duration",
504502
"ctime": "datetime",
505-
"identifier": "string",
503+
"id": "string",
506504
"kind": "string",
507505
"name": "string",
508506
"now_is": "datetime",
@@ -538,12 +536,12 @@ async def test_kinds_command(cli: CLI, foo_model: Model) -> None:
538536
async def test_sort_command(cli: CLI) -> None:
539537
async def identifiers(query: str) -> List[str]:
540538
result = await cli.execute_cli_command(query + " | dump", list_sink)
541-
return [r["reported"]["identifier"] for r in result[0]]
539+
return [r["reported"]["id"] for r in result[0]]
542540

543-
id_wo = await identifiers("search is(bla) | sort identifier")
544-
id_asc = await identifiers("search is(bla) | sort identifier asc")
545-
id_desc = await identifiers("search is(bla) | sort identifier desc")
546-
id_kind = await identifiers("search is(bla) | sort identifier | sort kind")
541+
id_wo = await identifiers("search is(bla) | sort id")
542+
id_asc = await identifiers("search is(bla) | sort id asc")
543+
id_desc = await identifiers("search is(bla) | sort id desc")
544+
id_kind = await identifiers("search is(bla) | sort id | sort kind")
547545
assert id_wo == id_asc
548546
assert id_wo == id_kind
549547
assert id_asc == list(reversed(id_desc))
@@ -553,27 +551,27 @@ async def identifiers(query: str) -> List[str]:
553551
async def test_limit_command(cli: CLI) -> None:
554552
async def identifiers(query: str) -> List[str]:
555553
result = await cli.execute_cli_command(query + " | dump", list_sink)
556-
return [r["reported"]["identifier"] for r in result[0]]
554+
return [r["reported"]["id"] for r in result[0]]
557555

558-
assert await identifiers("search is(bla) sort identifier | limit 1") == ["0_0"]
559-
assert await identifiers("search is(bla) sort identifier | limit 2") == ["0_0", "0_1"]
560-
assert await identifiers("search is(bla) sort identifier | limit 2, 2") == ["0_2", "0_3"]
561-
assert await identifiers("search is(bla) sort identifier | limit 10, 2") == ["1_0", "1_1"]
562-
assert await identifiers("search is(bla) sort identifier | limit 100, 2") == []
556+
assert await identifiers("search is(bla) sort id | limit 1") == ["0_0"]
557+
assert await identifiers("search is(bla) sort id | limit 2") == ["0_0", "0_1"]
558+
assert await identifiers("search is(bla) sort id | limit 2, 2") == ["0_2", "0_3"]
559+
assert await identifiers("search is(bla) sort id | limit 10, 2") == ["1_0", "1_1"]
560+
assert await identifiers("search is(bla) sort id | limit 100, 2") == []
563561

564562

565563
@pytest.mark.asyncio
566564
async def test_list_command(cli: CLI) -> None:
567-
result = await cli.execute_cli_command('search is (foo) and identifier=="4" sort some_int | list', list_sink)
565+
result = await cli.execute_cli_command('search is (foo) and id=="4" sort some_int | list', list_sink)
568566
assert len(result[0]) == 1
569-
assert result[0][0].startswith("kind=foo, identifier=4, some_int=0, age=")
567+
assert result[0][0].startswith("kind=foo, id=4, some_int=0, age=")
570568
list_cmd = "list some_int as si, some_string"
571-
result = await cli.execute_cli_command(f'search is (foo) and identifier=="4" | {list_cmd}', list_sink)
569+
result = await cli.execute_cli_command(f'search is (foo) and id=="4" | {list_cmd}', list_sink)
572570
assert result[0] == ["si=0, some_string=hello"]
573571

574572
# list is added automatically when no output renderer is defined and has the same behaviour as if it was given
575-
result = await cli.execute_cli_command('search is (foo) and identifier=="4" sort some_int', list_sink)
576-
assert result[0][0].startswith("kind=foo, identifier=4, some_int=0, age=")
573+
result = await cli.execute_cli_command('search is (foo) and id=="4" sort some_int', list_sink)
574+
assert result[0][0].startswith("kind=foo, id=4, some_int=0, age=")
577575

578576
# List is using the correct type
579577
props = dict(id="test", a="a", b=True, c=False, d=None, e=12, f=1.234, reported={})
@@ -964,7 +962,7 @@ async def test_pagerduty_alias(cli: CLI, echo_http_server: Tuple[int, List[Tuple
964962
"severity": "warning",
965963
"component": "Fix",
966964
"custom_details": {
967-
"collector": {"sub_root": {"no-region": {"0_0": {"id": None, "name": "yes or no", "kind": "bla"}}}}
965+
"collector": {"sub_root": {"no-region": {"0_0": {"id": "0_0", "name": "yes or no", "kind": "bla"}}}}
968966
},
969967
},
970968
"routing_key": "123",
@@ -1435,7 +1433,7 @@ async def exec(cmd: str) -> List[JsonElement]:
14351433
in_one_min = utc_str(now + timedelta(minutes=1))
14361434
one_min_ago = utc_str(now - timedelta(minutes=1))
14371435
# Create a time series based on all foo entries
1438-
res = await exec("timeseries snapshot --name test 'search aggregate(reported.some_int, reported.identifier: sum(1)): is(foo)'") # fmt: skip
1436+
res = await exec("timeseries snapshot --name test 'search aggregate(reported.some_int, reported.id: sum(1)): is(foo)'") # fmt: skip
14391437
assert res[0] == "10 entries added to time series test."
14401438
# Get the time series combined with each complete group
14411439
res = await exec(f"timeseries get --name test --start {one_min_ago} --end {in_one_min}")
@@ -1446,11 +1444,11 @@ async def exec(cmd: str) -> List[JsonElement]:
14461444
# Combine over some_int (which has only one) --> only one entry for one timestamp
14471445
res = await exec(f"timeseries get --name test --start {one_min_ago} --end {in_one_min} --group some_int")
14481446
assert len(res) == 1
1449-
# Combine over identifier (which is unique for each entry) --> 10 entries for one timestamp
1450-
res = await exec(f"timeseries get --name test --start {one_min_ago} --end {in_one_min} --group identifier")
1447+
# Combine over id (which is unique for each entry) --> 10 entries for one timestamp
1448+
res = await exec(f"timeseries get --name test --start {one_min_ago} --end {in_one_min} --group id")
14511449
assert len(res) == 10
1452-
# Combine over identifier (which is unique for each entry), filter for identifier==2 --> 1 entry for one timestamp
1453-
res = await exec(f'timeseries get --name test --start {one_min_ago} --end {in_one_min} --group identifier --filter identifier=="2"') # fmt: skip
1450+
# Combine over id (which is unique for each entry), filter for id==2 --> 1 entry for one timestamp
1451+
res = await exec(f'timeseries get --name test --start {one_min_ago} --end {in_one_min} --group id --filter id=="2"') # fmt: skip
14541452
assert len(res) == 1
14551453

14561454

fixcore/tests/fixcore/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def foo_kinds() -> List[Kind]:
263263
"base",
264264
[],
265265
[
266-
Property("identifier", "string", required=True),
266+
Property("id", "string", required=True),
267267
Property("kind", "string", required=True),
268268
Property("ctime", "datetime"),
269269
],
@@ -328,7 +328,7 @@ def person_model() -> Model:
328328
"Base",
329329
[],
330330
[
331-
Property("id", "string", required=True, description="Some identifier"),
331+
Property("id", "string", required=True, description="Some id"),
332332
Property("kind", "string", required=True, description="Kind of this node."),
333333
Property("list", "string[]", description="A list of strings."),
334334
Property("tags", "dictionary[string, string]", description="Key/value pairs."),
@@ -592,7 +592,7 @@ def benchmark() -> Benchmark:
592592
framework="test",
593593
documentation="test",
594594
version="1.5",
595-
clouds=["test"],
595+
clouds=["collector"],
596596
title="test_benchmark",
597597
description="test_benchmark",
598598
checks=[],

0 commit comments

Comments
 (0)