@cryptotaxi247 / CoPilot / commits / 6f75c39b

Scoutsuite (#234)

* scoutsuite integration * refactor: Update AWS ScoutSuite report generation process * refactor: Update AWS ScoutSuite command construction * refactor: Update AWS ScoutSuite command construction * refactor: Update AWS ScoutSuite command construction * refactor: Delete ScoutSuite report and associated files * refactor: Update AWS ScoutSuite command construction * added cloud-security-assessment * updated overview page breakpoints * added cloudSecurityAssessment api/types * added AvailableReportsItem component * refactor: Remove unnecessary code in create_customer_provisioning_default_settings * refactor: modify admin password creation to fix bug with special characters * refactor: Update admin password generation to use longer length * bug: update ProvisioningDefaultSettings * provision ha proxy bug fix * refactor * update: AvailableReportsList component * refactor: Update available report generation options in ScoutSuite API to only include aws for now * added azure and gcp back to scoutsuite * add CreationReportForm component * fix: getBaseUrl function * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Jun 7, 2024 at 13:19 UTC 6f75c39bb5c05b98d8b6563db468e55a961dcd30
32 files changed +811 -40
.gitignore
+1
@@ -55,6 +55,7 @@ backend/data/api.config.yaml
55 backend/file-store/api.config.yaml
56 backend/report.pdf
57 backend/report.html
58 +backend/scoutsuite-report
59 backend/app/integrations/office365/services/wazuh_config.xml
60
61 frontend/src/unplugin.components.d.ts
.vscode/settings.json
+2
@@ -4,6 +4,7 @@
4 "apexchart",
5 "arcticons",
6 "CARBONBLACK",
7 + "clickoutside",
8 "cmdline",
9 "colord",
10 "commonmark",
@@ -37,6 +38,7 @@
38 "Popselect",
39 "redoc",
40 "rushstack",
41 + "scoutsuite",
42 "Shiki",
43 "shikijs",
44 "signin",
backend/app/auth/models/users.py
+1 -3
@@ -118,20 +118,18 @@ class Password(BaseModel):
118 lowercase = string.ascii_lowercase
119 uppercase = string.ascii_uppercase
120 digits = string.digits
121 - punctuation = string.punctuation
121
122 # Ensure the password has at least one lowercase, one uppercase, one digit, and one symbol
123 password_chars = [
124 random.choice(lowercase),
125 random.choice(uppercase),
126 random.choice(digits),
128 - random.choice(punctuation),
127 ]
128
129 # Fill the rest of the password length with a random mix of characters
130 if length > 4:
131 password_chars += random.choices(
134 - lowercase + uppercase + digits + punctuation,
132 + lowercase + uppercase + digits,
133 k=length - 4,
134 )
135
backend/app/auth/services/universal.py
+1 -1
@@ -111,7 +111,7 @@ async def create_admin_user(session: AsyncSession):
111 session,
112 ): # The check function needs to be passed the session as well
113 # Create the admin user
114 - password_model = Password.generate(length=12)
114 + password_model = Password.generate(length=24)
115 admin_user = User(
116 username="admin",
117 password=password_model.hashed, # Assuming you store the hashed password
backend/app/customer_provisioning/routes/default_settings.py
-1
@@ -64,7 +64,6 @@ async def create_customer_provisioning_default_settings(
64
65 db.add(customer_provisioning_default_settings)
66 await db.commit()
67 - await db.refresh(customer_provisioning_default_settings)
67 return CustomerProvisioningDefaultSettingsResponse(
68 message="Customer Provisioning Default Settings created successfully",
69 success=True,
backend/app/customer_provisioning/services/provision.py
+2 -1
@@ -293,10 +293,11 @@ async def provision_haproxy(
293 """
294 logger.info(f"Provisioning HAProxy {request}")
295 api_endpoint = await get_connector_attribute(
296 - connector_id=16,
296 + connector_id=15,
297 column_name="connector_url",
298 session=session,
299 )
300 + logger.info(f"HAProxy API endpoint: {api_endpoint}")
301 # Send the POST request to the Wazuh worker
302 response = requests.post(
303 url=f"{api_endpoint}/provision_worker/haproxy",
backend/app/integrations/scoutsuite/routes/scoutsuite.py new
+122
@@ -0,0 +1,122 @@
1 +import os
2 +
3 +from fastapi import APIRouter
4 +from fastapi import BackgroundTasks
5 +from fastapi import HTTPException
6 +from loguru import logger
7 +
8 +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 ScoutSuiteReportOptions
13 +from app.integrations.scoutsuite.schema.scoutsuite import (
14 + ScoutSuiteReportOptionsResponse,
15 +)
16 +from app.integrations.scoutsuite.schema.scoutsuite import ScoutSuiteReportResponse
17 +from app.integrations.scoutsuite.services.scoutsuite import (
18 + generate_aws_report_background,
19 +)
20 +
21 +integration_scoutsuite_router = APIRouter()
22 +
23 +
24 +@integration_scoutsuite_router.get(
25 + "/report-generation-options",
26 + response_model=ScoutSuiteReportOptionsResponse,
27 + description="Get the available report generation options.",
28 +)
29 +async def get_report_generation_options():
30 + """
31 + Retrieves the available report generation options for ScoutSuite.
32 +
33 + Returns:
34 + ScoutSuiteReportOptionsResponse: The response containing the available report generation options.
35 + """
36 + return ScoutSuiteReportOptionsResponse(
37 + options=[ScoutSuiteReportOptions.aws, ScoutSuiteReportOptions.azure, ScoutSuiteReportOptions.gcp],
38 + success=True,
39 + message="ScoutSuite Report generation options retrieved successfully",
40 + )
41 +
42 +
43 +@integration_scoutsuite_router.get(
44 + "/available-reports",
45 + response_model=AvailableScoutSuiteReportsResponse,
46 + description="Get the available ScoutSuite reports.",
47 +)
48 +async def get_available_reports():
49 + """
50 + List all the `.html` files from the `scoutsuite-report` directory
51 +
52 + Returns:
53 + AvailableScoutSuiteReportsResponse: The response containing the list of available ScoutSuite reports.
54 + Raises:
55 + HTTPException: If the directory does not exist.
56 + """
57 + directory = "scoutsuite-report"
58 + full_path = os.path.abspath(directory)
59 +
60 + logger.info(f"Checking directory: {full_path}")
61 +
62 + if not os.path.exists(directory):
63 + raise HTTPException(status_code=404, detail="Directory does not exist")
64 +
65 + files = os.listdir(directory)
66 + html_files = [file for file in files if file.endswith(".html")]
67 +
68 + return AvailableScoutSuiteReportsResponse(
69 + available_reports=html_files,
70 + success=True,
71 + message="Available ScoutSuite reports retrieved successfully",
72 + )
73 +
74 +
75 +@integration_scoutsuite_router.post(
76 + "/generate-aws-report",
77 + response_model=ScoutSuiteReportResponse,
78 +)
79 +async def generate_aws_report(
80 + background_tasks: BackgroundTasks,
81 + request: AWSScoutSuiteReportRequest,
82 +):
83 + """
84 + Endpoint to generate an AWS ScoutSuite report.
85 +
86 + Args:
87 + background_tasks (BackgroundTasks): The background tasks object.
88 + request (AWSScoutSuiteReportRequest): The request object.
89 + session (AsyncSession): The async session object for database operations.
90 + """
91 + background_tasks.add_task(generate_aws_report_background, request)
92 + return ScoutSuiteReportResponse(
93 + success=True,
94 + message="AWS ScoutSuite report generation started successfully. This will take a few minutes to complete. Check back in shortly.",
95 + )
96 +
97 +
98 +@integration_scoutsuite_router.delete(
99 + "/delete-report/{report_name}",
100 + response_model=ScoutSuiteReportResponse,
101 +)
102 +async def delete_report(
103 + report_name: str,
104 +):
105 + """
106 + Endpoint to delete a ScoutSuite report.
107 +
108 + Args:
109 + report_name (str): The name of the report to delete.
110 + """
111 + report_base_name = os.path.splitext(report_name)[0]
112 + report_file_path = f"scoutsuite-report/{report_name}"
113 + exceptions_file_path = f"scoutsuite-report/scoutsuite-results/scoutsuite_exceptions_{report_base_name}.js"
114 + results_file_path = f"scoutsuite-report/scoutsuite-results/scoutsuite_results_{report_base_name}.js"
115 +
116 + files_to_delete = [report_file_path, exceptions_file_path, results_file_path]
117 +
118 + for file_path in files_to_delete:
119 + if os.path.exists(file_path):
120 + os.remove(file_path)
121 +
122 + return ScoutSuiteReportResponse(success=True, message=f"Report {report_name} and associated files deleted successfully")
backend/app/integrations/scoutsuite/schema/scoutsuite.py new
+48
@@ -0,0 +1,48 @@
1 +from enum import Enum
2 +from typing import List
3 +
4 +from fastapi import HTTPException
5 +from pydantic import BaseModel
6 +from pydantic import Field
7 +from pydantic import root_validator
8 +
9 +
10 +class ScoutSuiteReportOptions(str, Enum):
11 + aws = "aws"
12 + azure = "azure"
13 + gcp = "gcp"
14 +
15 +
16 +class ScoutSuiteReportOptionsResponse(BaseModel):
17 + options: List[ScoutSuiteReportOptions] = Field(
18 + ...,
19 + description="The available report generation options",
20 + example=["aws", "azure", "gcp"],
21 + )
22 + success: bool
23 + message: str
24 +
25 +
26 +class AWSScoutSuiteReportRequest(BaseModel):
27 + report_type: str = Field(..., description="The type of report to generate", example="aws")
28 + access_key_id: str = Field(..., description="The AWS access key ID", example="AKIAIOSFODNN7EXAMPLE")
29 + secret_access_key: str = Field(..., description="The AWS secret access key", example="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
30 + report_name: str = Field(..., description="The name of the report", example="aws-report")
31 +
32 + @root_validator
33 + def validate_report_type(cls, values):
34 + report_type = values.get("report_type")
35 + if report_type != ScoutSuiteReportOptions.aws:
36 + raise HTTPException(status_code=400, detail="Invalid report type.")
37 + return values
38 +
39 +
40 +class ScoutSuiteReportResponse(BaseModel):
41 + success: bool
42 + message: str
43 +
44 +
45 +class AvailableScoutSuiteReportsResponse(BaseModel):
46 + success: bool
47 + message: str
48 + available_reports: List[str]
backend/app/integrations/scoutsuite/services/scoutsuite.py new
+50
@@ -0,0 +1,50 @@
1 +import asyncio
2 +import subprocess
3 +from concurrent.futures import ThreadPoolExecutor
4 +
5 +from loguru import logger
6 +
7 +from app.integrations.scoutsuite.schema.scoutsuite import AWSScoutSuiteReportRequest
8 +
9 +
10 +async def generate_aws_report_background(request: AWSScoutSuiteReportRequest):
11 + logger.info("Generating AWS ScoutSuite report in the background")
12 +
13 + command = construct_aws_command(request)
14 + await run_command_in_background(command)
15 +
16 +
17 +def construct_aws_command(request: AWSScoutSuiteReportRequest):
18 + """Construct the scout command."""
19 + return [
20 + "scout",
21 + "aws",
22 + "--access-key-id",
23 + request.access_key_id,
24 + "--secret-access-key",
25 + request.secret_access_key,
26 + "--report-name",
27 + request.report_name,
28 + "--force",
29 + "--no-browser",
30 + ]
31 +
32 +
33 +def run_command(command):
34 + """Run the command and handle the output."""
35 + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
36 + stdout, stderr = process.communicate()
37 +
38 + if process.returncode != 0:
39 + logger.error(f"ScoutSuite report generation failed: {stderr.decode()}")
40 + return None
41 +
42 + logger.info("ScoutSuite report generated successfully")
43 + return None
44 +
45 +
46 +async def run_command_in_background(command):
47 + """Run the command in a separate thread."""
48 + with ThreadPoolExecutor() as executor:
49 + loop = asyncio.get_event_loop()
50 + await loop.run_in_executor(executor, lambda: run_command(command))
backend/app/routers/scoutsuite.py new
+13
@@ -0,0 +1,13 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.scoutsuite.routes.scoutsuite import integration_scoutsuite_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the ScoutSuite related routes
9 +router.include_router(
10 + integration_scoutsuite_router,
11 + prefix="/scoutsuite",
12 + tags=["ScoutSuite"],
13 +)
backend/copilot.py
+10
@@ -7,6 +7,7 @@ from fastapi import FastAPI
7 from fastapi import HTTPException
8 from fastapi.exceptions import RequestValidationError
9 from fastapi.middleware.cors import CORSMiddleware
10 +from fastapi.staticfiles import StaticFiles
11 from loguru import logger
12
13 from app.auth.utils import AuthHandler
@@ -55,6 +56,7 @@ from app.routers import network_connectors
56 from app.routers import office365
57 from app.routers import sap_siem
58 from app.routers import scheduler
59 +from app.routers import scoutsuite
60 from app.routers import shuffle
61 from app.routers import smtp
62 from app.routers import stack_provisioning
@@ -141,6 +143,7 @@ api_router.include_router(modules.router)
143 api_router.include_router(carbonblack.router)
144 api_router.include_router(network_connectors.router)
145 api_router.include_router(crowdstrike.router)
146 +api_router.include_router(scoutsuite.router)
147
148 # Include the APIRouter in the FastAPI app
149 app.include_router(api_router)
@@ -168,6 +171,13 @@ async def init_db():
171 scheduler.start()
172
173
174 +# Create `scoutsuite-report` directory if it doesnt exist
175 +if not os.path.exists("scoutsuite-report"):
176 + os.makedirs("scoutsuite-report")
177 +
178 +app.mount("/scoutsuite-report", StaticFiles(directory="scoutsuite-report"), name="scoutsuite-report")
179 +
180 +
181 @app.get("/")
182 def hello():
183 return {"message": "CoPilot - We Made It!"}
backend/requirements.txt
+1
@@ -137,6 +137,7 @@ rfc3339-validator==0.1.4
137 rfc3986-validator==0.1.1
138 rich==13.6.0
139 rsa==4.9
140 +ScoutSuite==5.14.0
141 setuptools==65.5.0
142 simplejson==3.19.1
143 six==1.16.0
frontend/package-lock.json
-1
@@ -14,7 +14,6 @@
14 "@fontsource/lexend": "^5.0.20",
15 "@fontsource/public-sans": "^5.0.18",
16 "@popperjs/core": "^2.11.8",
17 - "@rollup/rollup-linux-x64-gnu": "*",
17 "@shikijs/markdown-it": "^1.6.2",
18 "@vueuse/components": "^10.10.0",
19 "@vueuse/core": "^10.10.0",
frontend/package.json
+8 -8
@@ -3,11 +3,6 @@
3 "version": "1.0.0",
4 "private": true,
5 "type": "module",
6 - "overrides": {
7 - "secure-ls": {
8 - "crypto-js": "^4.2.0"
9 - }
10 - },
6 "scripts": {
7 "dev": "vite --host 0.0.0.0",
8 "dev:debug": "DEBUG=vite:* vite",
@@ -120,10 +115,15 @@
115 "vitest": "^1.6.0",
116 "vue-tsc": "^2.0.19"
117 },
123 - "engines": {
124 - "node": ">=18.0.0"
125 - },
118 "optionalDependencies": {
119 "@rollup/rollup-linux-x64-gnu": "^4.18.0"
120 + },
121 + "overrides": {
122 + "secure-ls": {
123 + "crypto-js": "^4.2.0"
124 + }
125 + },
126 + "engines": {
127 + "node": ">=18.0.0"
128 }
129 }
frontend/src/api/cloudSecurityAssessment.ts new
+20
@@ -0,0 +1,20 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "./httpClient"
3 +import type { ScoutSuiteReportPayload, ScoutSuiteReport } from "@/types/cloudSecurityAssessment.d"
4 +
5 +export default {
6 + getAvailableScoutSuiteReports() {
7 + return HttpClient.get<FlaskBaseResponse & { available_reports: ScoutSuiteReport[] }>(
8 + `/scoutsuite/available-reports`
9 + )
10 + },
11 + getScoutSuiteReportGenerationOptions() {
12 + return HttpClient.get<FlaskBaseResponse & { options: string[] }>(`/scoutsuite/report-generation-options`)
13 + },
14 + generateAwsScoutSuiteReport(payload: ScoutSuiteReportPayload) {
15 + return HttpClient.post<FlaskBaseResponse>(`/scoutsuite/generate-aws-report`, { ...payload, report_type: "aws" })
16 + },
17 + deleteScoutSuiteReport(reportName: string) {
18 + return HttpClient.delete<FlaskBaseResponse>(`/scoutsuite/delete-report/${reportName}`)
19 + }
20 +}
frontend/src/api/customers.ts
+5 -4
@@ -3,7 +3,7 @@ import { HttpClient } from "./httpClient"
3 import type {
4 Customer,
5 CustomerAgentHealth,
6 - CustomerDecomissionedData,
6 + CustomerDecommissionedData,
7 CustomerMeta,
8 CustomerProvision,
9 CustomerProvisioningDefaultSettings
@@ -17,6 +17,7 @@ export interface CustomerAgentsHealthcheckQuery {
17 }
18
19 export interface ProvisioningDefaultSettingsPayload {
20 + id: number
21 clusterName: string
22 clusterKey: string
23 masterIp: string
@@ -111,7 +112,7 @@ export default {
112 )
113 },
114 decommissionCustomer(code: string) {
114 - return HttpClient.post<FlaskBaseResponse & { decomissioned_data: CustomerDecomissionedData }>(
115 + return HttpClient.post<FlaskBaseResponse & { decomissioned_data: CustomerDecommissionedData }>(
116 `/customer_provisioning/decommission`,
117 {},
118 {
@@ -140,7 +141,7 @@ export default {
141 return HttpClient.post<
142 FlaskBaseResponse & { customer_provisioning_default_settings: CustomerProvisioningDefaultSettings }
143 >(`/customer_provisioning/default_settings`, {
143 - id: 0,
144 + id: payload.id,
145 cluster_name: payload.clusterName,
146 cluster_key: payload.clusterKey,
147 master_ip: payload.masterIp,
@@ -152,7 +153,7 @@ export default {
153 return HttpClient.put<
154 FlaskBaseResponse & { customer_provisioning_default_settings: CustomerProvisioningDefaultSettings }
155 >(`/customer_provisioning/default_settings`, {
155 - id: 0,
156 + id: payload.id,
157 cluster_name: payload.clusterName,
158 cluster_key: payload.clusterKey,
159 master_ip: payload.masterIp,
frontend/src/api/index.ts
+3 -1
@@ -20,6 +20,7 @@ import reporting from "./reporting"
20 import license from "./license"
21 import scheduler from "./scheduler"
22 import networkConnectors from "./networkConnectors"
23 +import cloudSecurityAssessment from "./cloudSecurityAssessment"
24
25 export default {
26 agents,
@@ -43,5 +44,6 @@ export default {
44 reporting,
45 license,
46 scheduler,
46 - networkConnectors
47 + networkConnectors,
48 + cloudSecurityAssessment
49 }
frontend/src/components/cloudSecurityAssessment/AvailableReportsItem.vue new
+94
@@ -0,0 +1,94 @@
1 +<template>
2 + <n-spin :show="canceling">
3 + <div class="item flex flex-col gap-2 px-5 py-3" @click="openReport()">
4 + <div class="header-box flex justify-between">
5 + {{ report }}
6 + </div>
7 + <div class="footer-box flex justify-end">
8 + <n-popconfirm
9 + @positive-click="deleteScoutSuiteReport()"
10 + v-model:show="showConfirm"
11 + trigger="manual"
12 + @clickoutside="showConfirm = false"
13 + >
14 + <template #trigger>
15 + <div @click.stop="showConfirm = true" class="delete-btn">delete</div>
16 + </template>
17 + Are you sure you want to delete the report?
18 + </n-popconfirm>
19 + </div>
20 + </div>
21 + </n-spin>
22 +</template>
23 +
24 +<script setup lang="ts">
25 +import type { ScoutSuiteReport } from "@/types/cloudSecurityAssessment.d"
26 +import Api from "@/api"
27 +import { ref } from "vue"
28 +import { useMessage, NPopconfirm, NSpin } from "naive-ui"
29 +import { getBaseUrl } from "@/utils"
30 +
31 +const emit = defineEmits<{
32 + (e: "deleted"): void
33 +}>()
34 +
35 +const { report } = defineProps<{ report: ScoutSuiteReport }>()
36 +const message = useMessage()
37 +const canceling = ref(false)
38 +const showConfirm = ref(false)
39 +
40 +function deleteScoutSuiteReport() {
41 + canceling.value = true
42 +
43 + Api.cloudSecurityAssessment
44 + .deleteScoutSuiteReport(report)
45 + .then(res => {
46 + if (res.data.success) {
47 + message.success(res.data?.message || "Report deleted successfully")
48 + emit("deleted")
49 + } else {
50 + message.warning(res.data?.message || "An error occurred. Please try again later.")
51 + }
52 + })
53 + .catch(err => {
54 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
55 + })
56 + .finally(() => {
57 + canceling.value = false
58 + })
59 +}
60 +
61 +function openReport() {
62 + window.open(`${getBaseUrl()}/scoutsuite-report/${report}`, "_blank")
63 +}
64 +</script>
65 +
66 +<style lang="scss" scoped>
67 +.item {
68 + border-radius: var(--border-radius);
69 + background-color: var(--bg-color);
70 + transition: all 0.2s var(--bezier-ease);
71 + border: var(--border-small-050);
72 + cursor: pointer;
73 +
74 + .header-box {
75 + font-family: var(--font-family-mono);
76 + }
77 + .footer-box {
78 + font-family: var(--font-family-mono);
79 + text-align: right;
80 + font-size: 12px;
81 + color: var(--fg-secondary-color);
82 +
83 + .delete-btn {
84 + &:hover {
85 + color: var(--error-color);
86 + }
87 + }
88 + }
89 +
90 + &:hover {
91 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
92 + }
93 +}
94 +</style>
frontend/src/components/cloudSecurityAssessment/AvailableReportsList.vue new
+109
@@ -0,0 +1,109 @@
1 +<template>
2 + <div class="available-reports-list">
3 + <div class="header flex items-center justify-end gap-2 mb-3">
4 + <div class="info grow flex gap-5">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total reports:
18 + <code>{{ totalReports }}</code>
19 + </div>
20 + </div>
21 + </n-popover>
22 + </div>
23 + <div class="actions flex gap-2 items-center">
24 + <n-button size="small" type="primary" @click="showForm = true">
25 + <template #icon>
26 + <Icon :name="NewReportIcon" :size="15"></Icon>
27 + </template>
28 + Create new Report
29 + </n-button>
30 + </div>
31 + </div>
32 + <n-spin :show="loading" class="min-h-32">
33 + <div class="list grid gap-4 grid-auto-flow-250">
34 + <template v-if="reportsList.length">
35 + <AvailableReportsItem
36 + v-for="report of reportsList"
37 + :key="report"
38 + :report
39 + class="item-appear item-appear-bottom item-appear-005"
40 + @deleted="getReports()"
41 + />
42 + </template>
43 + <template v-else>
44 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
45 + </template>
46 + </div>
47 + </n-spin>
48 +
49 + <n-modal
50 + v-model:show="showForm"
51 + display-directive="show"
52 + preset="card"
53 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
54 + title="Generate Report"
55 + :bordered="false"
56 + segmented
57 + >
58 + <CreationReportForm @submitted="getReports()" @mounted="formCTX = $event" />
59 + </n-modal>
60 + </div>
61 +</template>
62 +<script setup lang="ts">
63 +import { computed, onBeforeMount, ref, watch } from "vue"
64 +import { NSpin, NEmpty, NPopover, NButton, NModal, useMessage } from "naive-ui"
65 +import type { ScoutSuiteReport } from "@/types/cloudSecurityAssessment.d"
66 +import Icon from "@/components/common/Icon.vue"
67 +import AvailableReportsItem from "./AvailableReportsItem.vue"
68 +import CreationReportForm from "./CreationReportForm.vue"
69 +import Api from "@/api"
70 +
71 +const InfoIcon = "carbon:information"
72 +const NewReportIcon = "carbon:fetch-upload-cloud"
73 +const message = useMessage()
74 +const showForm = ref(false)
75 +const loading = ref(false)
76 +const reportsList = ref<ScoutSuiteReport[]>([])
77 +const totalReports = computed(() => reportsList.value.length)
78 +const formCTX = ref<{ reset: () => void } | null>(null)
79 +
80 +function getReports() {
81 + loading.value = true
82 +
83 + Api.cloudSecurityAssessment
84 + .getAvailableScoutSuiteReports()
85 + .then(res => {
86 + if (res.data.success) {
87 + reportsList.value = res.data?.available_reports || []
88 + } else {
89 + message.warning(res.data?.message || "An error occurred. Please try again later.")
90 + }
91 + })
92 + .catch(err => {
93 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
94 + })
95 + .finally(() => {
96 + loading.value = false
97 + })
98 +}
99 +
100 +watch(showForm, val => {
101 + if (val) {
102 + formCTX.value?.reset()
103 + }
104 +})
105 +
106 +onBeforeMount(() => {
107 + getReports()
108 +})
109 +</script>
frontend/src/components/cloudSecurityAssessment/CloudSecurityAssessmentButton.vue new
+26
@@ -0,0 +1,26 @@
1 +<template>
2 + <n-button :size="size" :type="type" @click="gotoPage()">
3 + <template #icon><Icon :name="CloudIcon"></Icon></template>
4 + Cloud Security Assessment
5 + </n-button>
6 +</template>
7 +
8 +<script setup lang="ts">
9 +import { NButton } from "naive-ui"
10 +import Icon from "@/components/common/Icon.vue"
11 +import type { Size, Type } from "naive-ui/es/button/src/interface"
12 +import { useRouter } from "vue-router"
13 +
14 +const { type, size } = defineProps<{
15 + size?: Size
16 + type?: Type
17 +}>()
18 +
19 +const CloudIcon = "carbon:cloud-data-ops"
20 +
21 +const router = useRouter()
22 +
23 +function gotoPage() {
24 + router.push({ name: "CloudSecurityAssessment" })
25 +}
26 +</script>
frontend/src/components/cloudSecurityAssessment/CreationReportForm.vue new
+245
@@ -0,0 +1,245 @@
1 +<template>
2 + <n-spin :show="loading" class="creation-report-form">
3 + <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
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"
9 + :options="reportTypeOptions"
10 + placeholder="Select..."
11 + clearable
12 + :loading="loadingOptions"
13 + />
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"
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>
41 +
42 + <div class="flex justify-between gap-4">
43 + <n-button @click="reset()" :disabled="loading">Reset</n-button>
44 + <n-button
45 + type="primary"
46 + :disabled="!isValid"
47 + @click="validate(() => submit())"
48 + :loading="submitting"
49 + >
50 + Submit
51 + </n-button>
52 + </div>
53 + </div>
54 + </n-form>
55 + </n-spin>
56 +</template>
57 +
58 +<script setup lang="ts">
59 +import { computed, onBeforeMount, onMounted, ref } from "vue"
60 +import Api from "@/api"
61 +import {
62 + useMessage,
63 + NForm,
64 + NFormItem,
65 + NInput,
66 + NButton,
67 + NSpin,
68 + NSelect,
69 + type FormValidationError,
70 + type FormInst,
71 + type FormRules,
72 + type MessageReactive
73 +} from "naive-ui"
74 +import type { ScoutSuiteReportPayload } from "@/types/cloudSecurityAssessment.d"
75 +
76 +type FormPayload = Omit<ScoutSuiteReportPayload, "report_type"> & { report_type: string | null }
77 +
78 +const emit = defineEmits<{
79 + (e: "submitted"): void
80 + (
81 + e: "mounted",
82 + value: {
83 + reset: () => void
84 + }
85 + ): void
86 +}>()
87 +
88 +const submitting = ref(false)
89 +const loadingOptions = ref(false)
90 +const loading = computed(() => submitting.value || loadingOptions.value)
91 +const message = useMessage()
92 +const form = ref<FormPayload>(getClearForm())
93 +const formRef = ref<FormInst | null>(null)
94 +
95 +const availableTypes = ["aws"]
96 +
97 +const reportTypeOptions = ref<{ label: string; value: string; disabled: boolean }[]>([])
98 +
99 +const rules: FormRules = {
100 + report_type: {
101 + required: true,
102 + message: "Please input the Report Type",
103 + trigger: ["input", "blur"]
104 + },
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 + },
115 + report_name: {
116 + required: true,
117 + message: "Please input the Report Name",
118 + trigger: ["input", "blur"]
119 + }
120 +}
121 +
122 +let validationMessage: MessageReactive | null = null
123 +
124 +const isValid = computed(() => {
125 + if (!form.value.access_key_id) {
126 + return false
127 + }
128 + if (!form.value.secret_access_key) {
129 + return false
130 + }
131 + if (!form.value.report_type) {
132 + return false
133 + }
134 + if (!form.value.report_name) {
135 + return false
136 + }
137 +
138 + return true
139 +})
140 +
141 +function validate(cb?: () => void) {
142 + if (!formRef.value) return
143 +
144 + formRef.value.validate((errors?: Array<FormValidationError>) => {
145 + if (!errors) {
146 + validationMessage?.destroy()
147 + validationMessage = null
148 + if (cb) cb()
149 + } else {
150 + if (!validationMessage) {
151 + validationMessage = message.warning("You must fill in the required fields correctly.")
152 + }
153 + return false
154 + }
155 + })
156 +}
157 +
158 +function getClearForm(): FormPayload {
159 + return {
160 + report_type: null,
161 + access_key_id: "",
162 + secret_access_key: "",
163 + report_name: ""
164 + }
165 +}
166 +
167 +function reset() {
168 + if (!loading.value) {
169 + resetForm()
170 + formRef.value?.restoreValidation()
171 + }
172 +}
173 +
174 +function resetForm() {
175 + form.value = getClearForm()
176 +}
177 +
178 +function submit() {
179 + const method = form.value.report_type === "aws" ? "generateAwsScoutSuiteReport" : null
180 +
181 + if (!method) {
182 + return
183 + }
184 +
185 + submitting.value = true
186 +
187 + const payload: ScoutSuiteReportPayload = {
188 + ...form.value,
189 + report_type: form.value.report_type || ""
190 + }
191 +
192 + Api.cloudSecurityAssessment[method](payload)
193 + .then(res => {
194 + if (res.data.success) {
195 + message.success(res.data?.message || `ScoutSuite report generation started successfully`, {
196 + duration: 10 * 1000
197 + })
198 + emit("submitted")
199 + resetForm()
200 + } else {
201 + message.warning(res.data?.message || "An error occurred. Please try again later.")
202 + }
203 + })
204 + .catch(err => {
205 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
206 + })
207 + .finally(() => {
208 + submitting.value = false
209 + })
210 +}
211 +
212 +function getScoutSuiteReportGenerationOptions() {
213 + loadingOptions.value = true
214 +
215 + Api.cloudSecurityAssessment
216 + .getScoutSuiteReportGenerationOptions()
217 + .then(res => {
218 + if (res.data.success) {
219 + reportTypeOptions.value = (res.data?.options || []).map(o => ({
220 + label: o.toUpperCase(),
221 + value: o,
222 + disabled: !availableTypes.includes(o)
223 + }))
224 + } else {
225 + message.warning(res.data?.message || "An error occurred. Please try again later.")
226 + }
227 + })
228 + .catch(err => {
229 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
230 + })
231 + .finally(() => {
232 + loadingOptions.value = false
233 + })
234 +}
235 +
236 +onBeforeMount(() => {
237 + getScoutSuiteReportGenerationOptions()
238 +})
239 +
240 +onMounted(() => {
241 + emit("mounted", {
242 + reset
243 + })
244 +})
245 +</script>
frontend/src/components/customers/CustomerForm.vue
+1 -1
@@ -3,7 +3,7 @@
3 <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
4 <div class="flex flex-col gap-4">
5 <div class="flex flex-wrap gap-4">
6 - <div v-for="(val, key) of form" :key="key" class="grow">
6 + <div v-for="(_, key) of form" :key="key" class="grow">
7 <n-form-item :label="fieldsMeta[key].label" :path="key" class="grow">
8 <n-input
9 v-model:value.trim="form[key]"
frontend/src/components/customers/CustomerMetaForm.vue
+1 -1
@@ -3,7 +3,7 @@
3 <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
4 <div class="flex flex-col gap-4">
5 <div class="flex flex-wrap gap-4">
6 - <div v-for="(val, key) of form" :key="key" class="grow">
6 + <div v-for="(_, key) of form" :key="key" class="grow">
7 <n-form-item :label="fieldsMeta[key].label" :path="key" class="grow">
8 <n-input
9 v-model:value.trim="form[key]"
frontend/src/components/customers/provision/CustomerDefaultSettingsForm.vue
+8 -6
@@ -71,7 +71,7 @@ const loading = computed(() => loadingDefaultSettings.value || submittingDefault
71 const message = useMessage()
72 const form = ref<Omit<CustomerProvisioningDefaultSettings, "id">>(getClearForm())
73 const formRef = ref<FormInst | null>(null)
74 -const isNew = ref(true)
74 +const entityId = ref(0)
75
76 const rules: FormRules = {
77 cluster_name: {
@@ -146,14 +146,15 @@ function validate() {
146 })
147 }
148
149 -function getClearForm(settings?: Omit<CustomerProvisioningDefaultSettings, "id">) {
150 - return {
149 +function getClearForm(settings?: Partial<CustomerProvisioningDefaultSettings>) {
150 + const payload = {
151 cluster_name: settings?.cluster_name || "",
152 cluster_key: settings?.cluster_key || "",
153 master_ip: settings?.master_ip || "",
154 grafana_url: settings?.grafana_url || "",
155 wazuh_worker_hostname: settings?.wazuh_worker_hostname || ""
156 }
157 + return payload
158 }
159
160 function reset() {
@@ -170,9 +171,10 @@ function resetForm() {
171 function submit() {
172 submittingDefaultSettings.value = true
173
173 - const method = isNew.value ? "setProvisioningDefaultSettings" : "updateProvisioningDefaultSettings"
174 + const method = entityId.value ? "updateProvisioningDefaultSettings" : "setProvisioningDefaultSettings"
175
176 const payload = {
177 + id: entityId.value || 0,
178 clusterName: form.value.cluster_name,
179 clusterKey: form.value.cluster_key,
180 masterIp: form.value.master_ip,
@@ -183,7 +185,7 @@ function submit() {
185 Api.customers[method](payload)
186 .then(res => {
187 if (res.data.success) {
186 - isNew.value = false
188 + entityId.value = res.data.customer_provisioning_default_settings.id
189 message.success(res.data?.message || "Customer Provisioning Default Settings updated successfully")
190 } else {
191 message.warning(res.data?.message || "An error occurred. Please try again later.")
@@ -224,7 +226,7 @@ function getProvisioningDefaultSettings() {
226 .getProvisioningDefaultSettings()
227 .then(res => {
228 if (res.data.success) {
227 - isNew.value = false
229 + entityId.value = res.data.customer_provisioning_default_settings.id || 0
230 setForm(res.data?.customer_provisioning_default_settings)
231 }
232 })
frontend/src/components/profile/ProfileSettings.vue
+2 -5
@@ -5,7 +5,7 @@
5 <div class="title">General</div>
6 <div class="flex flex-col md:flex-row md:gap-6">
7 <n-form-item label="Date Format" path="dateFormat" class="basis-1/3">
8 - <n-select v-model:value="formValue.dateFormat" :options="dateFormatsAvailables" />
8 + <n-select v-model:value="formValue.dateFormat" :options="dateFormatsAvailable" />
9 </n-form-item>
10 <n-form-item label="Time Format" path="hours24" class="basis-1/3">
11 <n-radio-group v-model:value="formValue.hours24" name="radiogroup">
@@ -47,14 +47,11 @@ const settingsStore = useSettingsStore()
47
48 const h24 = dayjs().format("HH:mm")
49 const h12 = dayjs().format("h:mm a")
50 -const dateFormatsAvailables = settingsStore.dateFormatsAvailables.map(i => ({ label: i, value: i }))
50 +const dateFormatsAvailable = settingsStore.dateFormatsAvailable.map(i => ({ label: i, value: i }))
51 const currentSateFormat = settingsStore.rawDateFormat
52 const hours24 = settingsStore.hours24
53
54 const formValue = ref({
55 - username: "sigmund67",
56 - email: "sigmund67@gmail.com",
57 - name: "Margie Dibbert",
55 dateFormat: currentSateFormat,
56 hours24
57 })
frontend/src/router/index.ts
+6
@@ -184,6 +184,12 @@ const router = createRouter({
184 component: () => import("@/views/Scheduler.vue"),
185 meta: { title: "Scheduler", auth: true, roles: UserRole.All }
186 },
187 + {
188 + path: "/cloud-security-assessment",
189 + name: "CloudSecurityAssessment",
190 + component: () => import("@/views/CloudSecurityAssessment.vue"),
191 + meta: { title: "Cloud Security Assessment", auth: true, roles: UserRole.All }
192 + },
193 {
194 path: "/license",
195 meta: {
frontend/src/stores/settings.ts
+1 -1
@@ -17,7 +17,7 @@ export const useSettingsStore = defineStore("settings", {
17 }
18 },
19 getters: {
20 - dateFormatsAvailables(state) {
20 + dateFormatsAvailable(state) {
21 return state.dateFormats
22 },
23 hours24(state) {
frontend/src/types/cloudSecurityAssessment.d.ts new
+8
@@ -0,0 +1,8 @@
1 +export type ScoutSuiteReport = string
2 +
3 +export interface ScoutSuiteReportPayload {
4 + report_type: string
5 + access_key_id: string
6 + secret_access_key: string
7 + report_name: string
8 +}
frontend/src/types/customers.d.ts
+1 -1
@@ -86,7 +86,7 @@ export interface CustomerProvision {
86 dfir_iris_username: string
87 }
88
89 -export interface CustomerDecomissionedData {
89 +export interface CustomerDecommissionedData {
90 agents_deleted: string[]
91 groups_deleted: string[]
92 stream_deleted: string
frontend/src/utils/index.ts
+5
@@ -4,6 +4,7 @@ import { isMobile as detectMobile } from "detect-touch-device"
4 import { md5 } from "js-md5"
5 import dayjs from "@/utils/dayjs"
6 import type { OsTypesFull } from "@/types/common"
7 +import _trim from "lodash/trim"
8
9 // Transform File Instance in base64 string
10 export function file2Base64(blob: Blob): Promise<string> {
@@ -109,3 +110,7 @@ export function price(
110
111 return `${symbol}${price}`
112 }
113 +
114 +export function getBaseUrl() {
115 + return _trim(import.meta.env.VITE_API_URL, "/")
116 +}
frontend/src/views/CloudSecurityAssessment.vue new
+9
@@ -0,0 +1,9 @@
1 +<template>
2 + <div class="page">
3 + <AvailableReportsList />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import AvailableReportsList from "@/components/cloudSecurityAssessment/AvailableReportsList.vue"
9 +</script>
frontend/src/views/Overview.vue
+8 -5
@@ -1,14 +1,15 @@
1 <template>
2 <div class="page" ref="page">
3 - <div class="section justify-end sm:justify-between flex gap-3">
4 - <div class="left-box hidden sm:flex gap-3">
3 + <div class="section justify-end md:justify-between flex gap-3">
4 + <div class="left-box hidden md:flex gap-3">
5 <StackProvisioningButton size="small" type="primary" />
6 + <CloudSecurityAssessmentButton size="small" type="primary" />
7 </div>
7 - <div class="right-box hidden sm:flex gap-3">
8 + <div class="right-box hidden md:flex gap-3">
9 <ActiveResponseWizardButton size="small" type="primary" />
10 <ThreatIntelButton size="small" type="primary" />
11 </div>
11 - <div class="mobile-box block sm:hidden">
12 + <div class="mobile-box block md:hidden">
13 <n-button size="small" type="primary" @click="showQuickActions = true">
14 <template #icon><Icon :name="QuickActionsIcon"></Icon></template>
15 Quick Actions
@@ -54,7 +55,7 @@
55
56 <n-drawer
57 v-model:show="showQuickActions"
57 - :width="250"
58 + :width="290"
59 style="max-width: 90vw"
60 :trap-focus="false"
61 display-directive="show"
@@ -62,6 +63,7 @@
63 <n-drawer-content title="Quick Actions" closable :native-scrollbar="false">
64 <div class="flex flex-col gap-3">
65 <StackProvisioningButton size="small" type="primary" />
66 + <CloudSecurityAssessmentButton size="small" type="primary" />
67 <ActiveResponseWizardButton size="small" type="primary" />
68 <ThreatIntelButton size="small" type="primary" />
69 </div>
@@ -79,6 +81,7 @@ import IndicesMarquee from "@/components/indices/Marquee.vue"
81 import ThreatIntelButton from "@/components/alerts/ThreatIntelButton.vue"
82 import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
83 import StackProvisioningButton from "@/components/stackProvisioning/StackProvisioningButton.vue"
84 +import CloudSecurityAssessmentButton from "@/components/cloudSecurityAssessment/CloudSecurityAssessmentButton.vue"
85 import AgentsCard from "@/components/overview/AgentsCard.vue"
86 import HealthcheckCard from "@/components/overview/HealthcheckCard.vue"
87 // import SocAlertsCard from "@/components/overview/SocAlertsCard.vue"