Azure scoutsuite (#246)
* feat: Add endpoint to generate Azure ScoutSuite report * chore: Update Wazuh agent vulnerabilities collection logic * update: eslint * update: dependencies * update: cloudSecurityAssessment api/types * update: common type * refactor: CreationReportForm * add: AzureTypeForm * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>
taylor_socfortress committed
Jun 18, 2024 at 17:54 UTC
a7f4af56a057738a08a9a5cfc6ab04942def5946
18 files changed
+463
-100
.pre-commit-config.yaml
-1
@@ -57,7 +57,6 @@ repos:
57
files: \.([cjt]sx?|[cm]ts|[cm]js|cvue)$ # *.js, *.jsx, *.ts, *.tsx and *.vue
58
args: ["--config", "frontend/eslint.config.js"]
59
additional_dependencies:
60
- - "@rushstack/eslint-patch@1.10.3"
60
- eslint@9.5.0
61
- "@vue/eslint-config-prettier@9.0.0"
62
- "@vue/eslint-config-typescript@13.0.0"
backend/app/agents/routes/agents.py
+2
@@ -30,6 +30,7 @@ from app.agents.wazuh.services.agents import upgrade_wazuh_agent
30
from app.agents.wazuh.services.sca import collect_agent_sca
31
from app.agents.wazuh.services.sca import collect_agent_sca_policy_results
32
from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities
33
+from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities_new
34
35
# App specific imports
36
from app.auth.routes.auth import AuthHandler
@@ -408,6 +409,7 @@ async def get_agent_vulnerabilities(agent_id: str) -> WazuhAgentVulnerabilitiesR
409
"""
410
logger.info(f"Fetching agent {agent_id} vulnerabilities")
411
return await collect_agent_vulnerabilities(agent_id)
412
+ # return await collect_agent_vulnerabilities_new(agent_id)
413
414
415
@agents_router.get(
backend/app/agents/wazuh/schema/agents.py
-3
@@ -36,12 +36,10 @@ class WazuhAgentsList(BaseModel):
36
37
class WazuhAgentVulnerabilities(BaseModel):
38
severity: Optional[str]
39
- updated: Optional[str]
39
version: Optional[str]
40
type: Optional[str]
41
name: Optional[str]
42
external_references: Optional[List[str]]
44
- condition: Optional[str]
43
detection_time: Optional[str]
44
cvss3_score: Optional[float]
45
published: Optional[str]
@@ -49,7 +47,6 @@ class WazuhAgentVulnerabilities(BaseModel):
47
cve: Optional[str]
48
status: Optional[str]
49
title: Optional[str]
52
- cvss2_score: Optional[float]
50
51
52
class WazuhAgentVulnerabilitiesResponse(BaseModel):
backend/app/agents/wazuh/services/vulnerabilities.py
+72
@@ -5,6 +5,8 @@ from loguru import logger
5
6
from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilities
7
from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
8
+from app.connectors.wazuh_indexer.utils.universal import collect_indices
9
+from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
10
from app.connectors.wazuh_manager.utils.universal import send_get_request
11
12
@@ -64,3 +66,73 @@ def process_agent_vulnerabilities(
66
status_code=500,
67
detail=f"Failed to process agent vulnerabilities: {e}",
68
)
69
+
70
+
71
+async def collect_agent_vulnerabilities_new(agent_id: str):
72
+ logger.info(f"Collecting agent {agent_id} vulnerabilities from Wazuh Indexer Index")
73
+ es = await create_wazuh_indexer_client("Wazuh-Indexer")
74
+ indices = await collect_indices(all_indices=True)
75
+ logger.info(f"Indices collect: {indices}")
76
+
77
+ vulnerabilities_indices = filter_vulnerabilities_indices(indices.indices_list)
78
+
79
+ agent_vulnerabilities = await collect_vulnerabilities(es, vulnerabilities_indices, agent_id)
80
+
81
+ processed_vulnerabilities = process_agent_vulnerabilities_new(agent_vulnerabilities)
82
+
83
+ return WazuhAgentVulnerabilitiesResponse(
84
+ vulnerabilities=processed_vulnerabilities,
85
+ success=True,
86
+ message="Vulnerabilities collected successfully",
87
+ )
88
+
89
+
90
+def filter_vulnerabilities_indices(indices_list):
91
+ return [index for index in indices_list if index.startswith("wazuh-states-vulnerabilities")]
92
+
93
+
94
+async def collect_vulnerabilities(es, vulnerabilities_indices, agent_id):
95
+ agent_vulnerabilities = []
96
+ for index in vulnerabilities_indices:
97
+ query = {"query": {"match": {"agent.id": agent_id}}}
98
+ response = es.search(index=index, body=query)
99
+
100
+ for hit in response["hits"]["hits"]:
101
+ vulnerability = hit["_source"]
102
+ agent_vulnerabilities.append(vulnerability)
103
+ return agent_vulnerabilities
104
+
105
+
106
+def process_agent_vulnerabilities_new(agent_vulnerabilities: List[dict]) -> List[WazuhAgentVulnerabilities]:
107
+ logger.info(f"Processing agent vulnerabilities: {agent_vulnerabilities}")
108
+
109
+ processed_vulnerabilities = []
110
+ for vulnerability in agent_vulnerabilities:
111
+ processed_vulnerability = process_single_vulnerability(vulnerability)
112
+ processed_vulnerabilities.append(processed_vulnerability)
113
+
114
+ return processed_vulnerabilities
115
+
116
+
117
+def process_single_vulnerability(vulnerability):
118
+ external_references = ensure_list(vulnerability.get("vulnerability").get("reference"))
119
+ return WazuhAgentVulnerabilities(
120
+ severity=vulnerability.get("vulnerability").get("severity"),
121
+ version=vulnerability.get("package").get("version"),
122
+ type=vulnerability.get("package").get("type"),
123
+ name=vulnerability.get("package").get("name"),
124
+ external_references=external_references,
125
+ detection_time=vulnerability.get("vulnerability").get("detected_at"),
126
+ cvss3_score=vulnerability.get("vulnerability").get("score").get("base"),
127
+ published=vulnerability.get("vulnerability").get("published_at"),
128
+ architecture=vulnerability.get("package").get("architecture"),
129
+ cve=vulnerability.get("vulnerability").get("id"),
130
+ status=vulnerability.get("status"),
131
+ title=vulnerability.get("vulnerability").get("description"),
132
+ )
133
+
134
+
135
+def ensure_list(value):
136
+ if not isinstance(value, list):
137
+ return [value]
138
+ return value
backend/app/connectors/wazuh_indexer/utils/universal.py
+32
-3
@@ -181,10 +181,38 @@ async def format_shards(shards):
181
]
182
183
184
-async def collect_indices() -> Indices:
184
+# async def collect_indices() -> Indices:
185
+# """
186
+# Collects the indices from Elasticsearch.
187
+
188
+# Returns:
189
+# dict: A dictionary containing the indices, shards, and indices stats.
190
+# """
191
+# logger.info("Collecting indices from Elasticsearch")
192
+# es = await create_wazuh_indexer_client("Wazuh-Indexer")
193
+# try:
194
+# indices_dict = es.indices.get_alias("*", expand_wildcards="open")
195
+# indices_list = list(indices_dict.keys())
196
+# # Check if the index is valid
197
+# index_config = IndexConfigModel()
198
+# indices_list = [index for index in indices_list if index_config.is_valid_index(index)]
199
+# return Indices(
200
+# indices_list=indices_list,
201
+# success=True,
202
+# message="Indices collected successfully",
203
+# )
204
+# except Exception as e:
205
+# logger.error(f"Failed to collect indices: {e}")
206
+# raise HTTPException(status_code=500, detail=f"Failed to collect indices: {e}")
207
+
208
+
209
+async def collect_indices(all_indices: bool = False) -> Indices:
210
"""
211
Collects the indices from Elasticsearch.
212
213
+ Args:
214
+ all_indices (bool, optional): If True, all indices are listed. If False, only valid indices are listed. Defaults to False.
215
+
216
Returns:
217
dict: A dictionary containing the indices, shards, and indices stats.
218
"""
@@ -194,8 +222,9 @@ async def collect_indices() -> Indices:
222
indices_dict = es.indices.get_alias("*", expand_wildcards="open")
223
indices_list = list(indices_dict.keys())
224
# Check if the index is valid
197
- index_config = IndexConfigModel()
198
- indices_list = [index for index in indices_list if index_config.is_valid_index(index)]
225
+ if not all_indices:
226
+ index_config = IndexConfigModel()
227
+ indices_list = [index for index in indices_list if index_config.is_valid_index(index)]
228
return Indices(
229
indices_list=indices_list,
230
success=True,
backend/app/integrations/scoutsuite/routes/scoutsuite.py
+27
@@ -9,6 +9,7 @@ from app.integrations.scoutsuite.schema.scoutsuite import (
9
AvailableScoutSuiteReportsResponse,
10
)
11
from app.integrations.scoutsuite.schema.scoutsuite import AWSScoutSuiteReportRequest
12
+from app.integrations.scoutsuite.schema.scoutsuite import AzureScoutSuiteReportRequest
13
from app.integrations.scoutsuite.schema.scoutsuite import ScoutSuiteReportOptions
14
from app.integrations.scoutsuite.schema.scoutsuite import (
15
ScoutSuiteReportOptionsResponse,
@@ -17,6 +18,9 @@ from app.integrations.scoutsuite.schema.scoutsuite import ScoutSuiteReportRespon
18
from app.integrations.scoutsuite.services.scoutsuite import (
19
generate_aws_report_background,
20
)
21
+from app.integrations.scoutsuite.services.scoutsuite import (
22
+ generate_azure_report_background,
23
+)
24
25
integration_scoutsuite_router = APIRouter()
26
@@ -95,6 +99,29 @@ async def generate_aws_report(
99
)
100
101
102
+@integration_scoutsuite_router.post(
103
+ "/generate-azure-report",
104
+ response_model=ScoutSuiteReportResponse,
105
+)
106
+async def generate_azure_report(
107
+ background_tasks: BackgroundTasks,
108
+ request: AzureScoutSuiteReportRequest,
109
+):
110
+ """
111
+ Endpoint to generate an Azure ScoutSuite report.
112
+
113
+ Args:
114
+ background_tasks (BackgroundTasks): The background tasks object.
115
+ request (AzureScoutSuiteReportRequest): The request object.
116
+ session (AsyncSession): The async session object for database operations.
117
+ """
118
+ background_tasks.add_task(generate_azure_report_background, request)
119
+ return ScoutSuiteReportResponse(
120
+ success=True,
121
+ message="Azure ScoutSuite report generation started successfully. This will take a few minutes to complete. Check back in shortly.",
122
+ )
123
+
124
+
125
@integration_scoutsuite_router.delete(
126
"/delete-report/{report_name}",
127
response_model=ScoutSuiteReportResponse,
backend/app/integrations/scoutsuite/schema/scoutsuite.py
+15
@@ -37,6 +37,21 @@ class AWSScoutSuiteReportRequest(BaseModel):
37
return values
38
39
40
+class AzureScoutSuiteReportRequest(BaseModel):
41
+ report_type: str = Field(..., description="The type of report to generate", example="azure")
42
+ username: str = Field(..., description="The username used to auth to Azure", example="scoutsuite@socfortress.co")
43
+ password: str = Field(..., description="The password used to auth to Azure", example="EXAMPLE_PASSWORD")
44
+ tenant_id: str = Field(..., description="The tenant ID used to auth to Azure", example="EXAMPLE_TENANT_ID")
45
+ report_name: str = Field(..., description="The name of the report", example="aws-report")
46
+
47
+ @root_validator
48
+ def validate_report_type(cls, values):
49
+ report_type = values.get("report_type")
50
+ if report_type != ScoutSuiteReportOptions.azure:
51
+ raise HTTPException(status_code=400, detail="Invalid report type.")
52
+ return values
53
+
54
+
55
class ScoutSuiteReportResponse(BaseModel):
56
success: bool
57
message: str
backend/app/integrations/scoutsuite/services/scoutsuite.py
+27
@@ -5,6 +5,7 @@ from concurrent.futures import ThreadPoolExecutor
5
from loguru import logger
6
7
from app.integrations.scoutsuite.schema.scoutsuite import AWSScoutSuiteReportRequest
8
+from app.integrations.scoutsuite.schema.scoutsuite import AzureScoutSuiteReportRequest
9
10
11
async def generate_aws_report_background(request: AWSScoutSuiteReportRequest):
@@ -30,6 +31,32 @@ def construct_aws_command(request: AWSScoutSuiteReportRequest):
31
]
32
33
34
+async def generate_azure_report_background(request: AzureScoutSuiteReportRequest):
35
+ logger.info("Generating Azure ScoutSuite report in the background")
36
+
37
+ command = construct_azure_command(request)
38
+ await run_command_in_background(command)
39
+
40
+
41
+def construct_azure_command(request: AzureScoutSuiteReportRequest):
42
+ """Construct the scout command."""
43
+ return [
44
+ "scout",
45
+ "azure",
46
+ "--user-account",
47
+ "--tenant",
48
+ request.tenant_id,
49
+ "--username",
50
+ request.username,
51
+ "--password",
52
+ request.password,
53
+ "--report-name",
54
+ request.report_name,
55
+ "--force",
56
+ "--no-browser",
57
+ ]
58
+
59
+
60
def run_command(command):
61
"""Run the command and handle the output."""
62
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
frontend/eslint.config.js
+15
-8
@@ -1,15 +1,16 @@
1
+import globals from "globals"
2
import path from "node:path"
3
import { fileURLToPath } from "node:url"
3
-import globals from "globals"
4
-
5
-import { FlatCompat } from "@eslint/eslintrc"
4
import js from "@eslint/js"
5
import pluginVue from "eslint-plugin-vue"
6
+import { FlatCompat } from "@eslint/eslintrc"
7
8
const __filename = fileURLToPath(import.meta.url)
9
const __dirname = path.dirname(__filename)
10
const compat = new FlatCompat({
12
- baseDirectory: __dirname
11
+ baseDirectory: __dirname,
12
+ recommendedConfig: js.configs.recommended,
13
+ allConfig: js.configs.all
14
})
15
16
export default [
@@ -17,9 +18,12 @@ export default [
18
ignores: ["**/dist/*", "**/tests/*", "**/.gitignore", "**/vite-env.d.ts"]
19
},
20
...pluginVue.configs["flat/essential"],
20
- js.configs.recommended,
21
- ...compat.extends("@vue/eslint-config-typescript/recommended"),
22
- ...compat.extends("@vue/eslint-config-prettier/skip-formatting"),
21
+ ...compat.extends(
22
+ "eslint:recommended",
23
+ "@typescript-eslint/recommended",
24
+ "@vue/eslint-config-typescript/recommended",
25
+ "@vue/eslint-config-prettier/skip-formatting"
26
+ ),
27
{
28
files: [
29
"**/*.vue",
@@ -42,7 +46,10 @@ export default [
46
},
47
rules: {
48
"vue/multi-word-component-names": "off",
45
- "vue/no-setup-props-destructure": "off"
49
+ "vue/no-setup-props-destructure": "off",
50
+ "no-redeclare": "off",
51
+ "@typescript-eslint/no-redeclare": "error",
52
+ "@typescript-eslint/adjacent-overload-signatures": "error"
53
}
54
}
55
]
frontend/package-lock.json
+11
-15
@@ -26,7 +26,7 @@
26
"echarts": "^5.5.0",
27
"file-saver": "^2.0.5",
28
"html-entities": "^2.5.2",
29
- "jose": "^5.4.0",
29
+ "jose": "^5.4.1",
30
"js-md5": "^0.8.3",
31
"lodash": "^4.17.21",
32
"mitt": "^3.0.1",
@@ -88,7 +88,7 @@
88
"type-fest": "^4.20.1",
89
"unplugin-vue-components": "^0.27.0",
90
"vite": "^5.3.1",
91
- "vite-bundle-analyzer": "^0.10.2",
91
+ "vite-bundle-analyzer": "^0.10.3",
92
"vite-bundle-visualizer": "^1.2.1",
93
"vite-plugin-vue-devtools": "^7.3.1",
94
"vite-svg-loader": "^5.1.0",
@@ -730,7 +730,6 @@
730
},
731
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
732
"version": "1.3.0",
733
- "extraneous": true,
733
"inBundle": true,
734
"license": "MIT",
735
"engines": {
@@ -6922,9 +6921,9 @@
6921
}
6922
},
6923
"node_modules/jose": {
6925
- "version": "5.4.0",
6926
- "resolved": "https://registry.npmjs.org/jose/-/jose-5.4.0.tgz",
6927
- "integrity": "sha512-6rpxTHPAQyWMb9A35BroFl1Sp0ST3DpPcm5EVIxZxdH+e0Hv9fwhyB3XLKFUcHNpdSDnETmBfuPPTTlYz5+USw==",
6924
+ "version": "5.4.1",
6925
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.4.1.tgz",
6926
+ "integrity": "sha512-U6QajmpV/nhL9SyfAewo000fkiRQ+Yd2H0lBxJJ9apjpOgkOcBQJWOrMo917lxLptdS/n/o/xPzMkXhF46K8hQ==",
6927
"funding": {
6928
"url": "https://github.com/sponsors/panva"
6929
}
@@ -10679,13 +10678,10 @@
10678
}
10679
},
10680
"node_modules/vite-bundle-analyzer": {
10682
- "version": "0.10.2",
10683
- "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.10.2.tgz",
10684
- "integrity": "sha512-fvjazb65+nQHyBaNvj2CQlKl+fCsvn87HKNAurB3epV7x24mBL9PoCGx9mbAn9sgqeT0fDTTAKi+yis7oWzoEA==",
10681
+ "version": "0.10.3",
10682
+ "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.10.3.tgz",
10683
+ "integrity": "sha512-Q5ko1EOu808q0mVv//H8OYd7yrijfEPUBlsg2NVx0rvbZ0ncUbnDh7UYqRmZWxaRDj0is4anE9DXatTiwZ1ZLg==",
10684
"dev": true,
10686
- "workspaces": [
10687
- "examples/**/*"
10688
- ],
10685
"dependencies": {
10686
"picocolors": "^1.0.0"
10687
}
@@ -11718,9 +11714,9 @@
11714
"dev": true
11715
},
11716
"node_modules/ws": {
11721
- "version": "8.17.0",
11722
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
11723
- "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
11717
+ "version": "8.17.1",
11718
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
11719
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
11720
"dev": true,
11721
"engines": {
11722
"node": ">=10.0.0"
frontend/package.json
+4
-4
@@ -48,7 +48,7 @@
48
"echarts": "^5.5.0",
49
"file-saver": "^2.0.5",
50
"html-entities": "^2.5.2",
51
- "jose": "^5.4.0",
51
+ "jose": "^5.4.1",
52
"js-md5": "^0.8.3",
53
"lodash": "^4.17.21",
54
"mitt": "^3.0.1",
@@ -72,7 +72,6 @@
72
"devDependencies": {
73
"@clack/prompts": "^0.7.0",
74
"@iconify/vue": "^4.1.2",
75
- "@rushstack/eslint-patch": "^1.10.3",
75
"@tsconfig/node20": "^20.1.4",
76
"@types/bytes": "^3.1.4",
77
"@types/file-saver": "^2.0.7",
@@ -110,7 +109,7 @@
109
"type-fest": "^4.20.1",
110
"unplugin-vue-components": "^0.27.0",
111
"vite": "^5.3.1",
113
- "vite-bundle-analyzer": "^0.10.2",
112
+ "vite-bundle-analyzer": "^0.10.3",
113
"vite-bundle-visualizer": "^1.2.1",
114
"vite-plugin-vue-devtools": "^7.3.1",
115
"vite-svg-loader": "^5.1.0",
@@ -132,7 +131,8 @@
131
},
132
"secure-ls": {
133
"crypto-js": "^4.2.0"
135
- }
134
+ },
135
+ "ws": "^8.17.1"
136
},
137
"engines": {
138
"node": ">=18.0.0"
frontend/src/api/cloudSecurityAssessment.ts
+13
-2
@@ -1,6 +1,11 @@
1
import { type FlaskBaseResponse } from "@/types/flask.d"
2
import { HttpClient } from "./httpClient"
3
-import type { ScoutSuiteReportPayload, ScoutSuiteReport } from "@/types/cloudSecurityAssessment.d"
3
+import type {
4
+ ScoutSuiteAwsReportPayload,
5
+ ScoutSuiteAzureReportPayload,
6
+ ScoutSuiteReport,
7
+ ScoutSuiteReportPayload
8
+} from "@/types/cloudSecurityAssessment.d"
9
10
export default {
11
getAvailableScoutSuiteReports() {
@@ -11,9 +16,15 @@ export default {
16
getScoutSuiteReportGenerationOptions() {
17
return HttpClient.get<FlaskBaseResponse & { options: string[] }>(`/scoutsuite/report-generation-options`)
18
},
14
- generateAwsScoutSuiteReport(payload: ScoutSuiteReportPayload) {
19
+ generateAwsScoutSuiteReport(payload: ScoutSuiteReportPayload & ScoutSuiteAwsReportPayload) {
20
return HttpClient.post<FlaskBaseResponse>(`/scoutsuite/generate-aws-report`, { ...payload, report_type: "aws" })
21
},
22
+ generateAzureScoutSuiteReport(payload: ScoutSuiteReportPayload & ScoutSuiteAzureReportPayload) {
23
+ return HttpClient.post<FlaskBaseResponse>(`/scoutsuite/generate-azure-report`, {
24
+ ...payload,
25
+ report_type: "azure"
26
+ })
27
+ },
28
deleteScoutSuiteReport(reportName: string) {
29
return HttpClient.delete<FlaskBaseResponse>(`/scoutsuite/delete-report/${reportName}`)
30
}
frontend/src/components/cloudSecurityAssessment/CreationReportForm.vue
+87
-61
@@ -1,11 +1,11 @@
1
<template>
2
<n-spin :show="loading" class="creation-report-form">
3
- <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
3
+ <n-form :model="baseForm" :rules="rules" ref="baseFormRef">
4
<div class="flex flex-col gap-2">
5
<div class="flex gap-4 items-start">
6
<n-form-item label="Type" path="report_type" class="w-32">
7
<n-select
8
- v-model:value="form.report_type"
8
+ v-model:value="baseForm.report_type"
9
:options="reportTypeOptions"
10
placeholder="Select..."
11
clearable
@@ -14,32 +14,28 @@
14
</n-form-item>
15
<n-form-item label="Name" path="report_name" class="grow">
16
<n-input
17
- v-model:value.trim="form.report_name"
17
+ v-model:value.trim="baseForm.report_name"
18
placeholder="Please insert Report Name"
19
clearable
20
/>
21
</n-form-item>
22
</div>
23
- <div class="flex flex-col gap-2">
24
- <n-form-item label="Access Key ID" path="access_key_id">
25
- <n-input
26
- v-model:value.trim="form.access_key_id"
27
- placeholder="Please insert Access Key ID"
28
- clearable
29
- />
30
- </n-form-item>
31
- <n-form-item label="Secret Access Key" path="secret_access_key">
32
- <n-input
33
- v-model:value.trim="form.secret_access_key"
34
- placeholder="Please insert Secret Access Key"
35
- type="password"
36
- show-password-on="click"
37
- clearable
38
- />
39
- </n-form-item>
40
- </div>
23
42
- <div class="flex justify-between gap-4">
24
+ <AwsTypeForm
25
+ v-if="baseForm.report_type === ScoutSuiteReportType.AWS"
26
+ @model="typeForm = $event"
27
+ @valid="typeFormValid = $event"
28
+ @mounted="typeFormRef = $event"
29
+ />
30
+
31
+ <AzureTypeForm
32
+ v-if="baseForm.report_type === ScoutSuiteReportType.Azure"
33
+ @model="typeForm = $event"
34
+ @valid="typeFormValid = $event"
35
+ @mounted="typeFormRef = $event"
36
+ />
37
+
38
+ <div class="flex justify-between gap-4 mt-8">
39
<n-button @click="reset()" :disabled="loading">Reset</n-button>
40
<n-button
41
type="primary"
@@ -56,7 +52,7 @@
52
</template>
53
54
<script setup lang="ts">
59
-import { computed, onBeforeMount, onMounted, ref } from "vue"
55
+import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
56
import Api from "@/api"
57
import {
58
useMessage,
@@ -71,9 +67,18 @@ import {
67
type FormRules,
68
type MessageReactive
69
} from "naive-ui"
74
-import type { ScoutSuiteReportPayload } from "@/types/cloudSecurityAssessment.d"
70
+import {
71
+ type ScoutSuiteAwsReportPayload,
72
+ type ScoutSuiteAzureReportPayload,
73
+ type ScoutSuiteReportPayload,
74
+ ScoutSuiteReportType
75
+} from "@/types/cloudSecurityAssessment.d"
76
+import AwsTypeForm from "./FormTypes/AwsTypeForm.vue"
77
+import AzureTypeForm from "./FormTypes/AzureTypeForm.vue"
78
+import type { ApiError, ApiCommonResponse } from "@/types/common"
79
76
-type FormPayload = Omit<ScoutSuiteReportPayload, "report_type"> & { report_type: string | null }
80
+type BaseFormPayload = Omit<ScoutSuiteReportPayload, "report_type"> & { report_type: ScoutSuiteReportType | null }
81
+type TypeFormPayload = ScoutSuiteAwsReportPayload | ScoutSuiteAzureReportPayload
82
83
const emit = defineEmits<{
84
(e: "submitted"): void
@@ -89,10 +94,13 @@ const submitting = ref(false)
94
const loadingOptions = ref(false)
95
const loading = computed(() => submitting.value || loadingOptions.value)
96
const message = useMessage()
92
-const form = ref<FormPayload>(getClearForm())
93
-const formRef = ref<FormInst | null>(null)
97
+const baseForm = ref<BaseFormPayload>(getClearBaseForm())
98
+const typeForm = ref<TypeFormPayload | null>(null)
99
+const typeFormValid = ref<boolean>(false)
100
+const baseFormRef = ref<FormInst | null>(null)
101
+const typeFormRef = ref<FormInst | null>(null)
102
95
-const availableTypes = ["aws"]
103
+const availableTypes = ["aws", "azure"]
104
105
const reportTypeOptions = ref<{ label: string; value: string; disabled: boolean }[]>([])
106
@@ -102,16 +110,6 @@ const rules: FormRules = {
110
message: "Please input the Report Type",
111
trigger: ["input", "blur"]
112
},
105
- access_key_id: {
106
- required: true,
107
- message: "Please input the Access Key ID",
108
- trigger: ["input", "blur"]
109
- },
110
- secret_access_key: {
111
- required: true,
112
- message: "Please input the Secret Access Key",
113
- trigger: ["input", "blur"]
114
- },
113
report_name: {
114
required: true,
115
message: "Please input the Report Name",
@@ -122,30 +120,48 @@ const rules: FormRules = {
120
let validationMessage: MessageReactive | null = null
121
122
const isValid = computed(() => {
125
- if (!form.value.access_key_id) {
123
+ if (!baseForm.value.report_type) {
124
return false
125
}
128
- if (!form.value.secret_access_key) {
126
+ if (!baseForm.value.report_name) {
127
return false
128
}
131
- if (!form.value.report_type) {
132
- return false
133
- }
134
- if (!form.value.report_name) {
129
+
130
+ if (!typeFormValid.value) {
131
return false
132
}
133
134
return true
135
})
136
137
+watch(
138
+ () => baseForm.value.report_type,
139
+ () => {
140
+ typeForm.value = null
141
+ typeFormRef.value = null
142
+ typeFormValid.value = false
143
+ }
144
+)
145
+
146
function validate(cb?: () => void) {
142
- if (!formRef.value) return
147
+ if (!baseFormRef.value || !typeFormRef.value) return
148
144
- formRef.value.validate((errors?: Array<FormValidationError>) => {
149
+ baseFormRef.value.validate((errors?: Array<FormValidationError>) => {
150
if (!errors) {
151
validationMessage?.destroy()
152
validationMessage = null
148
- if (cb) cb()
153
+ ;(typeFormRef.value as FormInst).validate((errors?: Array<FormValidationError>) => {
154
+ if (!errors) {
155
+ validationMessage?.destroy()
156
+ validationMessage = null
157
+ if (cb) cb()
158
+ } else {
159
+ if (!validationMessage) {
160
+ validationMessage = message.warning("You must fill in the required fields correctly.")
161
+ }
162
+ return false
163
+ }
164
+ })
165
} else {
166
if (!validationMessage) {
167
validationMessage = message.warning("You must fill in the required fields correctly.")
@@ -155,11 +171,9 @@ function validate(cb?: () => void) {
171
})
172
}
173
158
-function getClearForm(): FormPayload {
174
+function getClearBaseForm(): BaseFormPayload {
175
return {
176
report_type: null,
161
- access_key_id: "",
162
- secret_access_key: "",
177
report_name: ""
178
}
179
}
@@ -167,29 +181,41 @@ function getClearForm(): FormPayload {
181
function reset() {
182
if (!loading.value) {
183
resetForm()
170
- formRef.value?.restoreValidation()
184
+ baseFormRef.value?.restoreValidation()
185
}
186
}
187
188
function resetForm() {
175
- form.value = getClearForm()
189
+ baseForm.value = getClearBaseForm()
190
}
191
192
function submit() {
179
- const method = form.value.report_type === "aws" ? "generateAwsScoutSuiteReport" : null
193
+ let apiCall: Promise<ApiCommonResponse> | null = null
194
+
195
+ switch (baseForm.value.report_type) {
196
+ case ScoutSuiteReportType.AWS:
197
+ apiCall = Api.cloudSecurityAssessment.generateAwsScoutSuiteReport({
198
+ ...baseForm.value,
199
+ report_type: ScoutSuiteReportType.AWS,
200
+ ...(typeForm.value as ScoutSuiteAwsReportPayload)
201
+ })
202
+ break
203
+ case ScoutSuiteReportType.Azure:
204
+ apiCall = Api.cloudSecurityAssessment.generateAzureScoutSuiteReport({
205
+ ...baseForm.value,
206
+ report_type: ScoutSuiteReportType.Azure,
207
+ ...(typeForm.value as ScoutSuiteAzureReportPayload)
208
+ })
209
+ break
210
+ }
211
181
- if (!method) {
212
+ if (!apiCall) {
213
return
214
}
215
216
submitting.value = true
217
187
- const payload: ScoutSuiteReportPayload = {
188
- ...form.value,
189
- report_type: form.value.report_type || ""
190
- }
191
-
192
- Api.cloudSecurityAssessment[method](payload)
218
+ apiCall
219
.then(res => {
220
if (res.data.success) {
221
message.success(res.data?.message || `ScoutSuite report generation started successfully`, {
@@ -201,7 +227,7 @@ function submit() {
227
message.warning(res.data?.message || "An error occurred. Please try again later.")
228
}
229
})
204
- .catch(err => {
230
+ .catch((err: ApiError) => {
231
message.error(err.response?.data?.message || "An error occurred. Please try again later.")
232
})
233
.finally(() => {
frontend/src/components/cloudSecurityAssessment/FormTypes/AwsTypeForm.vue
new
+65
@@ -0,0 +1,65 @@
1
+<template>
2
+ <n-form :model="form" :rules="rules" ref="formRef">
3
+ <div class="flex flex-col gap-2">
4
+ <n-form-item label="Access Key ID" path="access_key_id">
5
+ <n-input v-model:value.trim="form.access_key_id" placeholder="Please insert Access Key ID" clearable />
6
+ </n-form-item>
7
+ <n-form-item label="Secret Access Key" path="secret_access_key">
8
+ <n-input
9
+ v-model:value.trim="form.secret_access_key"
10
+ placeholder="Please insert Secret Access Key"
11
+ type="password"
12
+ show-password-on="click"
13
+ clearable
14
+ />
15
+ </n-form-item>
16
+ </div>
17
+ </n-form>
18
+</template>
19
+
20
+<script setup lang="ts">
21
+import { computed, onMounted, ref, watch } from "vue"
22
+import { NForm, NFormItem, NInput, type FormRules, type FormInst } from "naive-ui"
23
+import type { ScoutSuiteAwsReportPayload } from "@/types/cloudSecurityAssessment.d"
24
+
25
+const emit = defineEmits<{
26
+ (e: "mounted", value: FormInst): void
27
+ (e: "model", value: ScoutSuiteAwsReportPayload): void
28
+ (e: "valid", value: boolean): void
29
+}>()
30
+
31
+const form = ref<ScoutSuiteAwsReportPayload>({
32
+ access_key_id: "",
33
+ secret_access_key: ""
34
+})
35
+const formRef = ref<FormInst>()
36
+
37
+const rules: FormRules = {
38
+ access_key_id: {
39
+ required: true,
40
+ message: "Please input the Access Key ID",
41
+ trigger: ["input", "blur"]
42
+ },
43
+ secret_access_key: {
44
+ required: true,
45
+ message: "Please input the Secret Access Key",
46
+ trigger: ["input", "blur"]
47
+ }
48
+}
49
+
50
+const isValid = computed(() => {
51
+ if (!form.value.access_key_id) return false
52
+ if (!form.value.secret_access_key) return false
53
+ return true
54
+})
55
+
56
+watch(form, val => emit("model", val), { deep: true, immediate: true })
57
+
58
+watch(isValid, val => emit("valid", val), { immediate: true })
59
+
60
+onMounted(() => {
61
+ if (formRef.value) {
62
+ emit("mounted", formRef.value)
63
+ }
64
+})
65
+</script>
frontend/src/components/cloudSecurityAssessment/FormTypes/AzureTypeForm.vue
new
+77
@@ -0,0 +1,77 @@
1
+<template>
2
+ <n-form :model="form" :rules="rules" ref="formRef">
3
+ <div class="flex flex-col gap-2">
4
+ <n-form-item label="Username" path="username">
5
+ <n-input v-model:value.trim="form.username" placeholder="Please insert Username" clearable />
6
+ </n-form-item>
7
+ <n-form-item label="Password" path="password">
8
+ <n-input
9
+ v-model:value.trim="form.password"
10
+ placeholder="Please insert Password"
11
+ type="password"
12
+ show-password-on="click"
13
+ clearable
14
+ />
15
+ </n-form-item>
16
+ <n-form-item label="Tenant ID" path="tenant_id">
17
+ <n-input v-model:value.trim="form.tenant_id" placeholder="Please insert Tenant ID" clearable />
18
+ </n-form-item>
19
+
20
+ <p class="text-center">ScoutSuite for Azure must be ran with a user where MFA is disabled</p>
21
+ </div>
22
+ </n-form>
23
+</template>
24
+
25
+<script setup lang="ts">
26
+import { computed, onMounted, ref, watch } from "vue"
27
+import { NForm, NFormItem, NInput, type FormRules, type FormInst } from "naive-ui"
28
+import type { ScoutSuiteAzureReportPayload } from "@/types/cloudSecurityAssessment.d"
29
+
30
+const emit = defineEmits<{
31
+ (e: "mounted", value: FormInst): void
32
+ (e: "model", value: ScoutSuiteAzureReportPayload): void
33
+ (e: "valid", value: boolean): void
34
+}>()
35
+
36
+const form = ref<ScoutSuiteAzureReportPayload>({
37
+ username: "",
38
+ password: "",
39
+ tenant_id: ""
40
+})
41
+const formRef = ref<FormInst>()
42
+
43
+const rules: FormRules = {
44
+ username: {
45
+ required: true,
46
+ message: "Please input the Username",
47
+ trigger: ["input", "blur"]
48
+ },
49
+ password: {
50
+ required: true,
51
+ message: "Please input the Password",
52
+ trigger: ["input", "blur"]
53
+ },
54
+ tenant_id: {
55
+ required: true,
56
+ message: "Please input the Tenant ID",
57
+ trigger: ["input", "blur"]
58
+ }
59
+}
60
+
61
+const isValid = computed(() => {
62
+ if (!form.value.username) return false
63
+ if (!form.value.password) return false
64
+ if (!form.value.tenant_id) return false
65
+ return true
66
+})
67
+
68
+watch(form, val => emit("model", val), { deep: true, immediate: true })
69
+
70
+watch(isValid, val => emit("valid", val), { immediate: true })
71
+
72
+onMounted(() => {
73
+ if (formRef.value) {
74
+ emit("mounted", formRef.value)
75
+ }
76
+})
77
+</script>
frontend/src/stores/theme.ts
+1
-1
@@ -470,7 +470,7 @@ export const useThemeStore = defineStore("theme", {
470
}
471
},
472
persist: {
473
- paths: ["layout", "themeName", "routerTransition", "boxed", "sidebar"]
473
+ paths: ["layout", "themeName", "routerTransition", "boxed", "sidebar.collapsed"]
474
}
475
})
476
frontend/src/types/cloudSecurityAssessment.d.ts
+13
-2
@@ -1,8 +1,19 @@
1
export type ScoutSuiteReport = string
2
3
+export enum ScoutSuiteReportType {
4
+ AWS = "aws",
5
+ Azure = "azure"
6
+}
7
export interface ScoutSuiteReportPayload {
4
- report_type: string
8
+ report_type: ScoutSuiteReportType
9
+ report_name: string
10
+}
11
+export interface ScoutSuiteAwsReportPayload {
12
access_key_id: string
13
secret_access_key: string
7
- report_name: string
14
+}
15
+export interface ScoutSuiteAzureReportPayload {
16
+ username: string
17
+ password: string
18
+ tenant_id: string
19
}
frontend/src/types/common.d.ts
+2
@@ -1,3 +1,5 @@
1
export type OsTypesFull = "Unknown" | "Windows" | "MacOS" | "UNIX" | "Linux"
2
export type OsTypesLower = "linux" | "windows" | "macos"
3
export type SafeAny = string | number | object
4
+export type ApiError = AxiosError<FlaskBaseResponse>
5
+export type ApiCommonResponse<T extends object = {}> = AxiosResponse<FlaskBaseResponse & T>