@cryptotaxi247 / CoPilot / commits / a7f4af56

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>