mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 12:31:54 +00:00
feat(attack-paths): add graph_data_ready field to decouple query availability from scan state (#10089)
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
@@ -21,6 +21,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
- Support CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
|
||||
- Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065)
|
||||
- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077)
|
||||
- Attack Paths: Add `graph_data_ready` field to decouple query availability from scan state [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"provider": "b85601a8-4b45-4194-8135-03fb980ef428",
|
||||
"scan": "01920573-aa9c-73c9-bcda-f2e35c9b19d2",
|
||||
"state": "completed",
|
||||
"graph_data_ready": true,
|
||||
"progress": 100,
|
||||
"update_tag": 1693586667,
|
||||
"task": null,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.15 on 2026-02-16 13:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0078_remove_attackpathsscan_graph_database_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="attackpathsscan",
|
||||
name="graph_data_ready",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Separate from 0079 because psqlextra's schema editor runs AddField DDL and DML
|
||||
# on different database connections, causing a deadlock when combined with RunPython
|
||||
# in the same migration.
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from api.db_router import MainRouter
|
||||
|
||||
|
||||
def backfill_graph_data_ready(apps, schema_editor):
|
||||
"""Set graph_data_ready=True for all completed AttackPathsScan rows."""
|
||||
AttackPathsScan = apps.get_model("api", "AttackPathsScan")
|
||||
AttackPathsScan.objects.using(MainRouter.admin_db).filter(
|
||||
state="completed",
|
||||
graph_data_ready=False,
|
||||
).update(graph_data_ready=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0079_attackpathsscan_graph_data_ready"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_graph_data_ready, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -655,6 +655,7 @@ class AttackPathsScan(RowLevelSecurityProtectedModel):
|
||||
|
||||
state = StateEnumField(choices=StateChoices.choices, default=StateChoices.AVAILABLE)
|
||||
progress = models.IntegerField(default=0)
|
||||
graph_data_ready = models.BooleanField(default=False)
|
||||
|
||||
# Timing
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
@@ -296,6 +296,7 @@ paths:
|
||||
enum:
|
||||
- state
|
||||
- progress
|
||||
- graph_data_ready
|
||||
- provider
|
||||
- provider_alias
|
||||
- provider_type
|
||||
@@ -355,7 +356,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -388,7 +389,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -571,6 +572,7 @@ paths:
|
||||
enum:
|
||||
- state
|
||||
- progress
|
||||
- graph_data_ready
|
||||
- provider
|
||||
- provider_alias
|
||||
- provider_type
|
||||
@@ -631,6 +633,7 @@ paths:
|
||||
enum:
|
||||
- state
|
||||
- progress
|
||||
- graph_data_ready
|
||||
- provider
|
||||
- provider_alias
|
||||
- provider_type
|
||||
@@ -1340,7 +1343,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -1373,7 +1376,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -1934,7 +1937,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -1967,7 +1970,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -2436,7 +2439,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -2469,7 +2472,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -2936,7 +2939,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -2969,7 +2972,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -3424,7 +3427,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -3457,7 +3460,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5253,7 +5256,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5286,7 +5289,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5420,7 +5423,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5453,7 +5456,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5763,7 +5766,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5796,7 +5799,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5964,7 +5967,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -5997,7 +6000,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -6395,7 +6398,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -6428,7 +6431,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -6561,7 +6564,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -6594,7 +6597,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -6751,7 +6754,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -6784,7 +6787,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -7582,7 +7585,7 @@ paths:
|
||||
name: filter[provider]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -7615,7 +7618,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -7650,7 +7653,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -7683,7 +7686,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -8341,7 +8344,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -8374,7 +8377,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -8847,7 +8850,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -8880,7 +8883,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -9166,7 +9169,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -9199,7 +9202,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -9491,7 +9494,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -9524,7 +9527,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -10350,7 +10353,7 @@ paths:
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -10383,7 +10386,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
enum:
|
||||
- alibabacloud
|
||||
- aws
|
||||
@@ -12596,16 +12599,16 @@ components:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
attribution:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AttackPathsQueryAttribution'
|
||||
nullable: true
|
||||
provider:
|
||||
type: string
|
||||
parameters:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AttackPathsQueryParameter'
|
||||
attribution:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AttackPathsQueryAttribution'
|
||||
nullable: true
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
@@ -12803,6 +12806,8 @@ components:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
graph_data_ready:
|
||||
type: boolean
|
||||
provider_alias:
|
||||
type: string
|
||||
readOnly: true
|
||||
@@ -17516,36 +17521,6 @@ components:
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g.,
|
||||
https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: 'Keystone API version (default: 3).'
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: 'User domain name (default: Default).'
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: 'Project domain name (default: Default).'
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret
|
||||
@@ -18560,7 +18535,7 @@ components:
|
||||
* `alibabacloud` - Alibaba Cloud
|
||||
* `cloudflare` - Cloudflare
|
||||
* `openstack` - OpenStack
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
uid:
|
||||
type: string
|
||||
title: Unique identifier for the provider, set by the provider
|
||||
@@ -18679,7 +18654,7 @@ components:
|
||||
- cloudflare
|
||||
- openstack
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
description: |-
|
||||
Type of provider to create.
|
||||
|
||||
@@ -18745,7 +18720,7 @@ components:
|
||||
- cloudflare
|
||||
- openstack
|
||||
type: string
|
||||
x-spec-enum-id: 2d8d323e9cc0044b
|
||||
x-spec-enum-id: 4b8815b179aa7216
|
||||
description: |-
|
||||
Type of provider to create.
|
||||
|
||||
@@ -19595,35 +19570,6 @@ components:
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g., https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: 'Keystone API version (default: 3).'
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: 'User domain name (default: Default).'
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: 'Project domain name (default: Default).'
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret_type
|
||||
@@ -20024,36 +19970,6 @@ components:
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g.,
|
||||
https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: 'Keystone API version (default: 3).'
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: 'User domain name (default: Default).'
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: 'Project domain name (default: Default).'
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret_type
|
||||
@@ -20465,35 +20381,6 @@ components:
|
||||
required:
|
||||
- clouds_yaml_content
|
||||
- clouds_yaml_cloud
|
||||
- type: object
|
||||
title: OpenStack Explicit Credentials
|
||||
properties:
|
||||
auth_url:
|
||||
type: string
|
||||
description: OpenStack Keystone authentication URL (e.g., https://openstack.example.com:5000/v3).
|
||||
username:
|
||||
type: string
|
||||
description: OpenStack username for authentication.
|
||||
password:
|
||||
type: string
|
||||
description: OpenStack password for authentication.
|
||||
region_name:
|
||||
type: string
|
||||
description: OpenStack region name (e.g., RegionOne).
|
||||
identity_api_version:
|
||||
type: string
|
||||
description: 'Keystone API version (default: 3).'
|
||||
user_domain_name:
|
||||
type: string
|
||||
description: 'User domain name (default: Default).'
|
||||
project_domain_name:
|
||||
type: string
|
||||
description: 'Project domain name (default: Default).'
|
||||
required:
|
||||
- auth_url
|
||||
- username
|
||||
- password
|
||||
- region_name
|
||||
writeOnly: true
|
||||
required:
|
||||
- secret
|
||||
|
||||
@@ -3922,6 +3922,7 @@ class TestAttackPathsScanViewSet:
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
provider,
|
||||
scan=scans_fixture[0],
|
||||
graph_data_ready=True,
|
||||
)
|
||||
query_definition = AttackPathsQueryDefinition(
|
||||
id="aws-rds",
|
||||
@@ -4000,7 +4001,7 @@ class TestAttackPathsScanViewSet:
|
||||
assert attributes["nodes"] == graph_payload["nodes"]
|
||||
assert attributes["relationships"] == graph_payload["relationships"]
|
||||
|
||||
def test_run_attack_paths_query_requires_completed_scan(
|
||||
def test_run_attack_paths_query_blocks_when_graph_data_not_ready(
|
||||
self,
|
||||
authenticated_client,
|
||||
providers_fixture,
|
||||
@@ -4012,6 +4013,7 @@ class TestAttackPathsScanViewSet:
|
||||
provider,
|
||||
scan=scans_fixture[0],
|
||||
state=StateChoices.EXECUTING,
|
||||
graph_data_ready=False,
|
||||
)
|
||||
|
||||
response = authenticated_client.post(
|
||||
@@ -4023,7 +4025,113 @@ class TestAttackPathsScanViewSet:
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "must be completed" in response.json()["errors"][0]["detail"]
|
||||
assert "not available" in response.json()["errors"][0]["detail"]
|
||||
|
||||
def test_run_attack_paths_query_allows_executing_scan_when_graph_data_ready(
|
||||
self,
|
||||
authenticated_client,
|
||||
providers_fixture,
|
||||
scans_fixture,
|
||||
create_attack_paths_scan,
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
provider,
|
||||
scan=scans_fixture[0],
|
||||
state=StateChoices.EXECUTING,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
query_definition = AttackPathsQueryDefinition(
|
||||
id="aws-test",
|
||||
name="Test",
|
||||
short_description="Test query.",
|
||||
description="Test query",
|
||||
provider=provider.provider,
|
||||
cypher="MATCH (n) RETURN n",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
with (
|
||||
patch("api.v1.views.get_query_by_id", return_value=query_definition),
|
||||
patch(
|
||||
"api.v1.views.attack_paths_views_helpers.prepare_query_parameters",
|
||||
return_value={"provider_uid": provider.uid},
|
||||
),
|
||||
patch(
|
||||
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
|
||||
return_value={
|
||||
"nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}],
|
||||
"relationships": [],
|
||||
},
|
||||
),
|
||||
patch("api.v1.views.graph_database.clear_cache"),
|
||||
patch(
|
||||
"api.v1.views.graph_database.get_database_name", return_value="db-test"
|
||||
),
|
||||
):
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"attack-paths-scans-queries-run",
|
||||
kwargs={"pk": attack_paths_scan.id},
|
||||
),
|
||||
data=self._run_payload("aws-test"),
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_run_attack_paths_query_allows_failed_scan_when_graph_data_ready(
|
||||
self,
|
||||
authenticated_client,
|
||||
providers_fixture,
|
||||
scans_fixture,
|
||||
create_attack_paths_scan,
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
provider,
|
||||
scan=scans_fixture[0],
|
||||
state=StateChoices.FAILED,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
query_definition = AttackPathsQueryDefinition(
|
||||
id="aws-test",
|
||||
name="Test",
|
||||
short_description="Test query.",
|
||||
description="Test query",
|
||||
provider=provider.provider,
|
||||
cypher="MATCH (n) RETURN n",
|
||||
parameters=[],
|
||||
)
|
||||
|
||||
with (
|
||||
patch("api.v1.views.get_query_by_id", return_value=query_definition),
|
||||
patch(
|
||||
"api.v1.views.attack_paths_views_helpers.prepare_query_parameters",
|
||||
return_value={"provider_uid": provider.uid},
|
||||
),
|
||||
patch(
|
||||
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
|
||||
return_value={
|
||||
"nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}],
|
||||
"relationships": [],
|
||||
},
|
||||
),
|
||||
patch("api.v1.views.graph_database.clear_cache"),
|
||||
patch(
|
||||
"api.v1.views.graph_database.get_database_name", return_value="db-test"
|
||||
),
|
||||
):
|
||||
response = authenticated_client.post(
|
||||
reverse(
|
||||
"attack-paths-scans-queries-run",
|
||||
kwargs={"pk": attack_paths_scan.id},
|
||||
),
|
||||
data=self._run_payload("aws-test"),
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_run_attack_paths_query_unknown_query(
|
||||
self,
|
||||
@@ -4036,6 +4144,7 @@ class TestAttackPathsScanViewSet:
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
provider,
|
||||
scan=scans_fixture[0],
|
||||
graph_data_ready=True,
|
||||
)
|
||||
|
||||
with patch("api.v1.views.get_query_by_id", return_value=None):
|
||||
@@ -4062,6 +4171,7 @@ class TestAttackPathsScanViewSet:
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
provider,
|
||||
scan=scans_fixture[0],
|
||||
graph_data_ready=True,
|
||||
)
|
||||
query_definition = AttackPathsQueryDefinition(
|
||||
id="aws-empty",
|
||||
|
||||
@@ -1145,6 +1145,7 @@ class AttackPathsScanSerializer(RLSSerializer):
|
||||
"id",
|
||||
"state",
|
||||
"progress",
|
||||
"graph_data_ready",
|
||||
"provider",
|
||||
"provider_alias",
|
||||
"provider_type",
|
||||
|
||||
@@ -2482,10 +2482,10 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
|
||||
def run_attack_paths_query(self, request, pk=None):
|
||||
attack_paths_scan = self.get_object()
|
||||
|
||||
if attack_paths_scan.state != StateChoices.COMPLETED:
|
||||
if not attack_paths_scan.graph_data_ready:
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "The Attack Paths scan must be completed before running Attack Paths queries"
|
||||
"detail": "Attack Paths data is not available for querying - a scan must complete at least once before queries can be run"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -28,12 +28,21 @@ def create_attack_paths_scan(
|
||||
return None
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
# Inherit graph_data_ready from the previous scan for this provider,
|
||||
# so queries remain available while the new scan runs.
|
||||
previous_data_ready = ProwlerAPIAttackPathsScan.objects.filter(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider_id,
|
||||
graph_data_ready=True,
|
||||
).exists()
|
||||
|
||||
attack_paths_scan = ProwlerAPIAttackPathsScan.objects.create(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider_id,
|
||||
scan_id=scan_id,
|
||||
state=StateChoices.SCHEDULED,
|
||||
started_at=datetime.now(tz=timezone.utc),
|
||||
graph_data_ready=previous_data_ready,
|
||||
)
|
||||
attack_paths_scan.save()
|
||||
|
||||
@@ -116,6 +125,32 @@ def update_attack_paths_scan_progress(
|
||||
attack_paths_scan.save(update_fields=["progress"])
|
||||
|
||||
|
||||
def set_graph_data_ready(
|
||||
attack_paths_scan: ProwlerAPIAttackPathsScan,
|
||||
ready: bool,
|
||||
) -> None:
|
||||
with rls_transaction(attack_paths_scan.tenant_id):
|
||||
attack_paths_scan.graph_data_ready = ready
|
||||
attack_paths_scan.save(update_fields=["graph_data_ready"])
|
||||
|
||||
|
||||
def set_provider_graph_data_ready(
|
||||
attack_paths_scan: ProwlerAPIAttackPathsScan,
|
||||
ready: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Set `graph_data_ready` for ALL scans of the same provider.
|
||||
|
||||
Used before drop/sync so that older scan IDs cannot bypass the query gate while the graph is being replaced.
|
||||
"""
|
||||
with rls_transaction(attack_paths_scan.tenant_id):
|
||||
ProwlerAPIAttackPathsScan.objects.filter(
|
||||
tenant_id=attack_paths_scan.tenant_id,
|
||||
provider_id=attack_paths_scan.provider_id,
|
||||
).update(graph_data_ready=ready)
|
||||
attack_paths_scan.refresh_from_db(fields=["graph_data_ready"])
|
||||
|
||||
|
||||
def fail_attack_paths_scan(
|
||||
tenant_id: str,
|
||||
scan_id: str,
|
||||
|
||||
@@ -169,6 +169,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
|
||||
sync.create_sync_indexes(tenant_neo4j_session)
|
||||
|
||||
logger.info(f"Deleting existing provider graph in {tenant_database_name}")
|
||||
db_utils.set_provider_graph_data_ready(attack_paths_scan, False)
|
||||
graph_database.drop_subgraph(
|
||||
database=tenant_database_name,
|
||||
provider_id=str(prowler_api_provider.id),
|
||||
@@ -183,6 +184,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
|
||||
target_database=tenant_database_name,
|
||||
provider_id=str(prowler_api_provider.id),
|
||||
)
|
||||
db_utils.set_graph_data_ready(attack_paths_scan, True)
|
||||
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 99)
|
||||
|
||||
logger.info(f"Clearing Neo4j cache for database {tenant_database_name}")
|
||||
|
||||
@@ -28,6 +28,8 @@ class TestAttackPathsRun:
|
||||
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
|
||||
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
|
||||
)
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
|
||||
@@ -72,6 +74,8 @@ class TestAttackPathsRun:
|
||||
mock_starting,
|
||||
mock_update_progress,
|
||||
mock_finish,
|
||||
mock_set_provider_graph_data_ready,
|
||||
mock_set_graph_data_ready,
|
||||
mock_event_loop,
|
||||
mock_drop_db,
|
||||
tenants_fixture,
|
||||
@@ -159,9 +163,66 @@ class TestAttackPathsRun:
|
||||
mock_finish.assert_called_once_with(
|
||||
attack_paths_scan, StateChoices.COMPLETED, ingestion_result
|
||||
)
|
||||
mock_set_provider_graph_data_ready.assert_called_once_with(
|
||||
attack_paths_scan, False
|
||||
)
|
||||
mock_set_graph_data_ready.assert_called_once_with(attack_paths_scan, True)
|
||||
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
|
||||
return_value="Cartography failed: ingestion boom",
|
||||
)
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
|
||||
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
|
||||
)
|
||||
@patch("tasks.jobs.attack_paths.scan.graph_database.drop_database")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
|
||||
@patch("tasks.jobs.attack_paths.scan.findings.analysis")
|
||||
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
|
||||
@patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes")
|
||||
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
|
||||
@patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run")
|
||||
@patch("tasks.jobs.attack_paths.scan.graph_database.create_database")
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
|
||||
return_value="db-scan-id",
|
||||
)
|
||||
@patch("tasks.jobs.attack_paths.scan.graph_database.get_uri")
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
|
||||
return_value=MagicMock(_enabled_regions=["us-east-1"]),
|
||||
)
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
)
|
||||
def test_run_failure_marks_scan_failed(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
self,
|
||||
mock_init_provider,
|
||||
mock_get_uri,
|
||||
mock_get_db_name,
|
||||
mock_create_db,
|
||||
mock_cartography_indexes,
|
||||
mock_cartography_analysis,
|
||||
mock_findings_indexes,
|
||||
mock_internet_analysis,
|
||||
mock_findings_analysis,
|
||||
mock_starting,
|
||||
mock_update_progress,
|
||||
mock_set_provider_graph_data_ready,
|
||||
mock_set_graph_data_ready,
|
||||
mock_finish,
|
||||
mock_drop_db,
|
||||
mock_event_loop,
|
||||
mock_stringify,
|
||||
tenants_fixture,
|
||||
providers_fixture,
|
||||
scans_fixture,
|
||||
):
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
@@ -185,53 +246,18 @@ class TestAttackPathsRun:
|
||||
ingestion_fn = MagicMock(side_effect=RuntimeError("ingestion boom"))
|
||||
|
||||
with (
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
|
||||
return_value=MagicMock(_enabled_regions=["us-east-1"]),
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.graph_database.get_uri"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
|
||||
return_value="db-scan-id",
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.graph_database.create_database"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.get_session",
|
||||
return_value=session_ctx,
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run"),
|
||||
patch("tasks.jobs.attack_paths.scan.cartography_analysis.run"),
|
||||
patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes"),
|
||||
patch("tasks.jobs.attack_paths.scan.internet.analysis"),
|
||||
patch("tasks.jobs.attack_paths.scan.findings.analysis"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.db_utils.retrieve_attack_paths_scan",
|
||||
return_value=attack_paths_scan,
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress"
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan"
|
||||
) as mock_finish,
|
||||
patch("tasks.jobs.attack_paths.scan.graph_database.drop_database"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.get_cartography_ingestion_function",
|
||||
return_value=ingestion_fn,
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
|
||||
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
|
||||
return_value="Cartography failed: ingestion boom",
|
||||
),
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="ingestion boom"):
|
||||
attack_paths_run(str(tenant.id), str(scan.id), "task-456")
|
||||
@@ -241,8 +267,64 @@ class TestAttackPathsRun:
|
||||
assert failure_args[1] == StateChoices.FAILED
|
||||
assert failure_args[2] == {"global_error": "Cartography failed: ingestion boom"}
|
||||
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
|
||||
return_value="Cartography failed: ingestion boom",
|
||||
)
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
|
||||
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
|
||||
)
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.drop_database",
|
||||
side_effect=ConnectionError("neo4j down"),
|
||||
)
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
|
||||
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
|
||||
@patch("tasks.jobs.attack_paths.scan.findings.analysis")
|
||||
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
|
||||
@patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes")
|
||||
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
|
||||
@patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run")
|
||||
@patch("tasks.jobs.attack_paths.scan.graph_database.create_database")
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
|
||||
return_value="db-scan-id",
|
||||
)
|
||||
@patch("tasks.jobs.attack_paths.scan.graph_database.get_uri")
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
|
||||
return_value=MagicMock(_enabled_regions=["us-east-1"]),
|
||||
)
|
||||
@patch(
|
||||
"tasks.jobs.attack_paths.scan.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
)
|
||||
def test_run_failure_marks_scan_failed_even_when_drop_database_fails(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
self,
|
||||
mock_init_provider,
|
||||
mock_get_uri,
|
||||
mock_get_db_name,
|
||||
mock_create_db,
|
||||
mock_cartography_indexes,
|
||||
mock_cartography_analysis,
|
||||
mock_findings_indexes,
|
||||
mock_internet_analysis,
|
||||
mock_findings_analysis,
|
||||
mock_starting,
|
||||
mock_update_progress,
|
||||
mock_set_provider_graph_data_ready,
|
||||
mock_set_graph_data_ready,
|
||||
mock_finish,
|
||||
mock_drop_db,
|
||||
mock_event_loop,
|
||||
mock_stringify,
|
||||
tenants_fixture,
|
||||
providers_fixture,
|
||||
scans_fixture,
|
||||
):
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
@@ -266,56 +348,18 @@ class TestAttackPathsRun:
|
||||
ingestion_fn = MagicMock(side_effect=RuntimeError("ingestion boom"))
|
||||
|
||||
with (
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
|
||||
return_value=MagicMock(_enabled_regions=["us-east-1"]),
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.graph_database.get_uri"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
|
||||
return_value="db-scan-id",
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.graph_database.create_database"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.get_session",
|
||||
return_value=session_ctx,
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run"),
|
||||
patch("tasks.jobs.attack_paths.scan.cartography_analysis.run"),
|
||||
patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes"),
|
||||
patch("tasks.jobs.attack_paths.scan.internet.analysis"),
|
||||
patch("tasks.jobs.attack_paths.scan.findings.analysis"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.db_utils.retrieve_attack_paths_scan",
|
||||
return_value=attack_paths_scan,
|
||||
),
|
||||
patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan"),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress"
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan"
|
||||
) as mock_finish,
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.graph_database.drop_database",
|
||||
side_effect=ConnectionError("neo4j down"),
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.get_cartography_ingestion_function",
|
||||
return_value=ingestion_fn,
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
|
||||
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
|
||||
),
|
||||
patch(
|
||||
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
|
||||
return_value="Cartography failed: ingestion boom",
|
||||
),
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="ingestion boom"):
|
||||
attack_paths_run(str(tenant.id), str(scan.id), "task-789")
|
||||
@@ -1017,3 +1061,317 @@ class TestInternetAnalysis:
|
||||
result = internet_module.analysis(mock_session, provider, config)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAttackPathsDbUtilsGraphDataReady:
|
||||
"""Tests for db_utils functions related to graph_data_ready lifecycle."""
|
||||
|
||||
def test_create_attack_paths_scan_first_scan_defaults_to_false(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
provider.provider = Provider.ProviderChoices.AWS
|
||||
provider.save()
|
||||
scan = scans_fixture[0]
|
||||
scan.provider = provider
|
||||
scan.save()
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
str(tenant.id), str(scan.id), provider.id
|
||||
)
|
||||
|
||||
assert attack_paths_scan is not None
|
||||
assert attack_paths_scan.graph_data_ready is False
|
||||
|
||||
def test_create_attack_paths_scan_inherits_true_from_previous(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
provider.provider = Provider.ProviderChoices.AWS
|
||||
provider.save()
|
||||
scan = scans_fixture[0]
|
||||
scan.provider = provider
|
||||
scan.save()
|
||||
|
||||
AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider,
|
||||
scan=scan,
|
||||
state=StateChoices.COMPLETED,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
|
||||
new_scan = Scan.objects.create(
|
||||
name="New Scan",
|
||||
provider=provider,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.AVAILABLE,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
str(tenant.id), str(new_scan.id), provider.id
|
||||
)
|
||||
|
||||
assert attack_paths_scan is not None
|
||||
assert attack_paths_scan.graph_data_ready is True
|
||||
|
||||
def test_create_attack_paths_scan_inherits_false_when_no_previous_ready(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
provider.provider = Provider.ProviderChoices.AWS
|
||||
provider.save()
|
||||
scan = scans_fixture[0]
|
||||
scan.provider = provider
|
||||
scan.save()
|
||||
|
||||
AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider,
|
||||
scan=scan,
|
||||
state=StateChoices.FAILED,
|
||||
graph_data_ready=False,
|
||||
)
|
||||
|
||||
new_scan = Scan.objects.create(
|
||||
name="New Scan",
|
||||
provider=provider,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.AVAILABLE,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
attack_paths_scan = create_attack_paths_scan(
|
||||
str(tenant.id), str(new_scan.id), provider.id
|
||||
)
|
||||
|
||||
assert attack_paths_scan is not None
|
||||
assert attack_paths_scan.graph_data_ready is False
|
||||
|
||||
def test_set_graph_data_ready_updates_field(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import set_graph_data_ready
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
provider.provider = Provider.ProviderChoices.AWS
|
||||
provider.save()
|
||||
scan = scans_fixture[0]
|
||||
scan.provider = provider
|
||||
scan.save()
|
||||
|
||||
attack_paths_scan = AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider,
|
||||
scan=scan,
|
||||
state=StateChoices.EXECUTING,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
set_graph_data_ready(attack_paths_scan, False)
|
||||
|
||||
attack_paths_scan.refresh_from_db()
|
||||
assert attack_paths_scan.graph_data_ready is False
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
set_graph_data_ready(attack_paths_scan, True)
|
||||
|
||||
attack_paths_scan.refresh_from_db()
|
||||
assert attack_paths_scan.graph_data_ready is True
|
||||
|
||||
def test_finish_attack_paths_scan_does_not_modify_graph_data_ready(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
provider.provider = Provider.ProviderChoices.AWS
|
||||
provider.save()
|
||||
scan = scans_fixture[0]
|
||||
scan.provider = provider
|
||||
scan.save()
|
||||
|
||||
attack_paths_scan = AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider,
|
||||
scan=scan,
|
||||
state=StateChoices.EXECUTING,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
finish_attack_paths_scan(attack_paths_scan, StateChoices.COMPLETED, {})
|
||||
|
||||
attack_paths_scan.refresh_from_db()
|
||||
assert attack_paths_scan.state == StateChoices.COMPLETED
|
||||
assert attack_paths_scan.graph_data_ready is True
|
||||
|
||||
def test_finish_attack_paths_scan_preserves_graph_data_ready_on_failure(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
provider.provider = Provider.ProviderChoices.AWS
|
||||
provider.save()
|
||||
scan = scans_fixture[0]
|
||||
scan.provider = provider
|
||||
scan.save()
|
||||
|
||||
attack_paths_scan = AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider,
|
||||
scan=scan,
|
||||
state=StateChoices.EXECUTING,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
finish_attack_paths_scan(
|
||||
attack_paths_scan,
|
||||
StateChoices.FAILED,
|
||||
{"global_error": "boom"},
|
||||
)
|
||||
|
||||
attack_paths_scan.refresh_from_db()
|
||||
assert attack_paths_scan.state == StateChoices.FAILED
|
||||
assert attack_paths_scan.graph_data_ready is True
|
||||
|
||||
def test_set_provider_graph_data_ready_updates_all_scans_for_provider(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider = providers_fixture[0]
|
||||
provider.provider = Provider.ProviderChoices.AWS
|
||||
provider.save()
|
||||
|
||||
scan_a = scans_fixture[0]
|
||||
scan_a.provider = provider
|
||||
scan_a.save()
|
||||
|
||||
scan_b = Scan.objects.create(
|
||||
name="Second Scan",
|
||||
provider=provider,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.AVAILABLE,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
old_ap_scan = AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider,
|
||||
scan=scan_a,
|
||||
state=StateChoices.COMPLETED,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
new_ap_scan = AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider,
|
||||
scan=scan_b,
|
||||
state=StateChoices.EXECUTING,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
set_provider_graph_data_ready(new_ap_scan, False)
|
||||
|
||||
old_ap_scan.refresh_from_db()
|
||||
new_ap_scan.refresh_from_db()
|
||||
assert old_ap_scan.graph_data_ready is False
|
||||
assert new_ap_scan.graph_data_ready is False
|
||||
|
||||
def test_set_provider_graph_data_ready_does_not_affect_other_providers(
|
||||
self, tenants_fixture, providers_fixture, scans_fixture
|
||||
):
|
||||
from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready
|
||||
|
||||
tenant = tenants_fixture[0]
|
||||
provider_a = providers_fixture[0]
|
||||
provider_a.provider = Provider.ProviderChoices.AWS
|
||||
provider_a.save()
|
||||
|
||||
provider_b = providers_fixture[1]
|
||||
provider_b.provider = Provider.ProviderChoices.AWS
|
||||
provider_b.save()
|
||||
|
||||
scan_a = scans_fixture[0]
|
||||
scan_a.provider = provider_a
|
||||
scan_a.save()
|
||||
|
||||
scan_b = Scan.objects.create(
|
||||
name="Scan for provider B",
|
||||
provider=provider_b,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
ap_scan_a = AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider_a,
|
||||
scan=scan_a,
|
||||
state=StateChoices.EXECUTING,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
ap_scan_b = AttackPathsScan.objects.create(
|
||||
tenant_id=tenant.id,
|
||||
provider=provider_b,
|
||||
scan=scan_b,
|
||||
state=StateChoices.COMPLETED,
|
||||
graph_data_ready=True,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tasks.jobs.attack_paths.db_utils.rls_transaction",
|
||||
new=lambda *args, **kwargs: nullcontext(),
|
||||
):
|
||||
set_provider_graph_data_ready(ap_scan_a, False)
|
||||
|
||||
ap_scan_a.refresh_from_db()
|
||||
ap_scan_b.refresh_from_db()
|
||||
assert ap_scan_a.graph_data_ready is False
|
||||
assert ap_scan_b.graph_data_ready is True
|
||||
|
||||
@@ -13,6 +13,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
|
||||
- Updated GitHub provider form placeholder to clarify both username and organization names are valid inputs [(#9830)](https://github.com/prowler-cloud/prowler/pull/9830)
|
||||
- CSA CCM detailed view and small fix related with `Top Failed Sections` width [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
|
||||
- Attack Paths: Show scan data availability status with badges and tooltips, allow selecting scans for querying while a new scan is in progress [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
|
||||
+13
-10
@@ -30,7 +30,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ProviderType } from "@/types";
|
||||
import type { AttackPathScan } from "@/types/attack-paths";
|
||||
import type { AttackPathScan, ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
import { ScanStatusBadge } from "./scan-status-badge";
|
||||
@@ -42,6 +42,11 @@ interface ScanListTableProps {
|
||||
const TABLE_COLUMN_COUNT = 6;
|
||||
const DEFAULT_PAGE_SIZE = 5;
|
||||
const PAGE_SIZE_OPTIONS = [2, 5, 10, 15];
|
||||
const WAITING_STATES: readonly ScanState[] = [
|
||||
SCAN_STATES.SCHEDULED,
|
||||
SCAN_STATES.AVAILABLE,
|
||||
SCAN_STATES.EXECUTING,
|
||||
];
|
||||
|
||||
const baseLinkClass =
|
||||
"relative block rounded border-0 bg-transparent px-3 py-1.5 text-button-primary outline-none transition-all duration-300 hover:bg-bg-neutral-tertiary hover:text-text-neutral-primary focus:shadow-none dark:hover:bg-bg-neutral-secondary dark:hover:text-text-neutral-primary";
|
||||
@@ -77,20 +82,17 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
};
|
||||
|
||||
const isSelectDisabled = (scan: AttackPathScan) => {
|
||||
return (
|
||||
scan.attributes.state !== SCAN_STATES.COMPLETED ||
|
||||
selectedScanId === scan.id
|
||||
);
|
||||
return !scan.attributes.graph_data_ready || selectedScanId === scan.id;
|
||||
};
|
||||
|
||||
const getSelectButtonLabel = (scan: AttackPathScan) => {
|
||||
if (selectedScanId === scan.id) {
|
||||
return "Selected";
|
||||
}
|
||||
if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
|
||||
return "Scheduled";
|
||||
if (scan.attributes.graph_data_ready) {
|
||||
return "Select";
|
||||
}
|
||||
if (scan.attributes.state === SCAN_STATES.EXECUTING) {
|
||||
if (WAITING_STATES.includes(scan.attributes.state)) {
|
||||
return "Waiting...";
|
||||
}
|
||||
if (scan.attributes.state === SCAN_STATES.FAILED) {
|
||||
@@ -184,6 +186,7 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
<ScanStatusBadge
|
||||
status={scan.attributes.state}
|
||||
progress={scan.attributes.progress}
|
||||
graphDataReady={scan.attributes.graph_data_ready}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -342,8 +345,8 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-6 text-xs">
|
||||
Only Attack Paths scans with "Completed" status can be
|
||||
selected. Scans in progress will update automatically.
|
||||
Scans can be selected when data is available. A new scan does not
|
||||
interrupt access to existing data.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
+72
-35
@@ -3,57 +3,94 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import type { ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
const BADGE_CONFIG: Record<
|
||||
ScanState,
|
||||
{ className: string; label: string; showGraphDot: boolean }
|
||||
> = {
|
||||
[SCAN_STATES.SCHEDULED]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Scheduled",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.AVAILABLE]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Queued",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.EXECUTING]: {
|
||||
className: "bg-bg-warning-secondary text-text-neutral-primary",
|
||||
label: "In Progress",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.COMPLETED]: {
|
||||
className: "bg-bg-pass-secondary text-text-success-primary",
|
||||
label: "Completed",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.FAILED]: {
|
||||
className: "bg-bg-fail-secondary text-text-error-primary",
|
||||
label: "Failed",
|
||||
showGraphDot: true,
|
||||
},
|
||||
};
|
||||
|
||||
interface ScanStatusBadgeProps {
|
||||
status: ScanState;
|
||||
progress?: number;
|
||||
graphDataReady?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status badge for attack path scan status
|
||||
* Shows visual indicator and text for scan progress
|
||||
*/
|
||||
export const ScanStatusBadge = ({
|
||||
status,
|
||||
progress = 0,
|
||||
graphDataReady = false,
|
||||
}: ScanStatusBadgeProps) => {
|
||||
if (status === "scheduled") {
|
||||
return (
|
||||
<Badge className="bg-bg-neutral-tertiary text-text-neutral-primary gap-2">
|
||||
<span>Scheduled</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const config = BADGE_CONFIG[status];
|
||||
|
||||
if (status === "available") {
|
||||
return (
|
||||
<Badge className="bg-bg-neutral-tertiary text-text-neutral-primary gap-2">
|
||||
<span>Queued</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const graphDot = graphDataReady && config.showGraphDot && (
|
||||
<span className="inline-block size-2 rounded-full bg-green-500" />
|
||||
);
|
||||
|
||||
if (status === "executing") {
|
||||
return (
|
||||
<Badge className="bg-bg-warning-secondary text-text-neutral-primary gap-2">
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
<span>In Progress ({progress}%)</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const tooltipText = graphDataReady
|
||||
? "Graph available"
|
||||
: status === SCAN_STATES.FAILED || status === SCAN_STATES.COMPLETED
|
||||
? "Graph not available"
|
||||
: "Graph not available yet";
|
||||
|
||||
if (status === "completed") {
|
||||
return (
|
||||
<Badge className="bg-bg-pass-secondary text-text-success-primary gap-2">
|
||||
<span>Completed</span>
|
||||
</Badge>
|
||||
const icon =
|
||||
status === SCAN_STATES.EXECUTING ? (
|
||||
<Loader2
|
||||
size={14}
|
||||
className={
|
||||
graphDataReady ? "animate-spin text-green-500" : "animate-spin"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
graphDot
|
||||
);
|
||||
}
|
||||
|
||||
const label =
|
||||
status === SCAN_STATES.EXECUTING
|
||||
? `${config.label} (${progress}%)`
|
||||
: config.label;
|
||||
|
||||
return (
|
||||
<Badge className="bg-bg-fail-secondary text-text-error-primary gap-2">
|
||||
<span>Failed</span>
|
||||
</Badge>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className={`${config.className} gap-2`}>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ export type ProviderType = (typeof PROVIDER_TYPES)[keyof typeof PROVIDER_TYPES];
|
||||
export interface AttackPathScanAttributes {
|
||||
state: ScanState;
|
||||
progress: number;
|
||||
graph_data_ready: boolean;
|
||||
provider_alias: string;
|
||||
provider_type: ProviderType;
|
||||
provider_uid: string;
|
||||
|
||||
+90
-77
@@ -1,93 +1,106 @@
|
||||
import { PaginationLinks, RelationshipWrapper } from "./attack-paths";
|
||||
import { ProviderType } from "./providers";
|
||||
|
||||
export interface ScannerArgs {
|
||||
only_logs?: boolean;
|
||||
excluded_checks?: string[];
|
||||
aws_retries_max_attempts?: number;
|
||||
}
|
||||
|
||||
export const SCAN_TRIGGER = {
|
||||
SCHEDULED: "scheduled",
|
||||
MANUAL: "manual",
|
||||
} as const;
|
||||
|
||||
export type ScanTrigger = (typeof SCAN_TRIGGER)[keyof typeof SCAN_TRIGGER];
|
||||
|
||||
export const SCAN_STATE = {
|
||||
AVAILABLE: "available",
|
||||
SCHEDULED: "scheduled",
|
||||
EXECUTING: "executing",
|
||||
COMPLETED: "completed",
|
||||
FAILED: "failed",
|
||||
CANCELLED: "cancelled",
|
||||
} as const;
|
||||
|
||||
export type ScanState = (typeof SCAN_STATE)[keyof typeof SCAN_STATE];
|
||||
|
||||
export interface ScanAttributes {
|
||||
name: string;
|
||||
trigger: ScanTrigger;
|
||||
state: ScanState;
|
||||
unique_resource_count: number;
|
||||
progress: number;
|
||||
scanner_args: ScannerArgs | null;
|
||||
duration: number;
|
||||
started_at: string;
|
||||
inserted_at: string;
|
||||
completed_at: string;
|
||||
scheduled_at: string;
|
||||
next_scan_at: string;
|
||||
}
|
||||
|
||||
export interface ScanRelationships {
|
||||
provider: RelationshipWrapper;
|
||||
task: RelationshipWrapper;
|
||||
}
|
||||
|
||||
export interface ScanProviderInfo {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export interface ScanProps {
|
||||
type: "scans";
|
||||
id: string;
|
||||
attributes: {
|
||||
name: string;
|
||||
trigger: "scheduled" | "manual";
|
||||
state:
|
||||
| "available"
|
||||
| "scheduled"
|
||||
| "executing"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled";
|
||||
unique_resource_count: number;
|
||||
progress: number;
|
||||
scanner_args: {
|
||||
only_logs?: boolean;
|
||||
excluded_checks?: string[];
|
||||
aws_retries_max_attempts?: number;
|
||||
} | null;
|
||||
duration: number;
|
||||
started_at: string;
|
||||
inserted_at: string;
|
||||
completed_at: string;
|
||||
scheduled_at: string;
|
||||
next_scan_at: string;
|
||||
};
|
||||
relationships: {
|
||||
provider: {
|
||||
data: {
|
||||
id: string;
|
||||
type: "providers";
|
||||
};
|
||||
};
|
||||
task: {
|
||||
data: {
|
||||
id: string;
|
||||
type: "tasks";
|
||||
};
|
||||
};
|
||||
};
|
||||
providerInfo?: {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
attributes: ScanAttributes;
|
||||
relationships: ScanRelationships;
|
||||
providerInfo?: ScanProviderInfo;
|
||||
}
|
||||
|
||||
export interface ScanEntityProviderInfo {
|
||||
provider: ProviderType;
|
||||
alias?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export interface ScanEntityAttributes {
|
||||
name?: string;
|
||||
completed_at: string;
|
||||
}
|
||||
|
||||
export interface ScanEntity {
|
||||
id: string;
|
||||
providerInfo: {
|
||||
provider: ProviderType;
|
||||
alias?: string;
|
||||
uid?: string;
|
||||
};
|
||||
attributes: {
|
||||
name?: string;
|
||||
completed_at: string;
|
||||
};
|
||||
providerInfo: ScanEntityProviderInfo;
|
||||
attributes: ScanEntityAttributes;
|
||||
}
|
||||
|
||||
export interface ExpandedScanData extends ScanProps {
|
||||
providerInfo: {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
providerInfo: ScanProviderInfo;
|
||||
}
|
||||
|
||||
export interface IncludedResource {
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: any;
|
||||
relationships?: any;
|
||||
}
|
||||
|
||||
export interface ApiPagination {
|
||||
page: number;
|
||||
pages: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ScansApiMeta {
|
||||
pagination: ApiPagination;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ScansApiResponse {
|
||||
links: {
|
||||
first: string;
|
||||
last: string;
|
||||
next: string | null;
|
||||
prev: string | null;
|
||||
};
|
||||
links: PaginationLinks;
|
||||
data: ScanProps[];
|
||||
included?: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: any;
|
||||
relationships?: any;
|
||||
}>;
|
||||
meta: {
|
||||
pagination: {
|
||||
page: number;
|
||||
pages: number;
|
||||
count: number;
|
||||
};
|
||||
version: string;
|
||||
};
|
||||
included?: IncludedResource[];
|
||||
meta: ScansApiMeta;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user