Skip to content

Commit

Permalink
Scoutsuite (#234)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
taylorwalton and Linko91 authored Jun 7, 2024
1 parent 8de81ec commit 6f75c39
Show file tree
Hide file tree
Showing 32 changed files with 811 additions and 40 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ backend/data/api.config.yaml
backend/file-store/api.config.yaml
backend/report.pdf
backend/report.html
backend/scoutsuite-report
backend/app/integrations/office365/services/wazuh_config.xml

frontend/src/unplugin.components.d.ts
Expand Down
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"apexchart",
"arcticons",
"CARBONBLACK",
"clickoutside",
"cmdline",
"colord",
"commonmark",
Expand Down Expand Up @@ -37,6 +38,7 @@
"Popselect",
"redoc",
"rushstack",
"scoutsuite",
"Shiki",
"shikijs",
"signin",
Expand Down
4 changes: 1 addition & 3 deletions backend/app/auth/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,18 @@ def generate(cls, length: int = 12) -> "Password":
lowercase = string.ascii_lowercase
uppercase = string.ascii_uppercase
digits = string.digits
punctuation = string.punctuation

# Ensure the password has at least one lowercase, one uppercase, one digit, and one symbol
password_chars = [
random.choice(lowercase),
random.choice(uppercase),
random.choice(digits),
random.choice(punctuation),
]

# Fill the rest of the password length with a random mix of characters
if length > 4:
password_chars += random.choices(
lowercase + uppercase + digits + punctuation,
lowercase + uppercase + digits,
k=length - 4,
)

Expand Down
2 changes: 1 addition & 1 deletion backend/app/auth/services/universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def create_admin_user(session: AsyncSession):
session,
): # The check function needs to be passed the session as well
# Create the admin user
password_model = Password.generate(length=12)
password_model = Password.generate(length=24)
admin_user = User(
username="admin",
password=password_model.hashed, # Assuming you store the hashed password
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ async def create_customer_provisioning_default_settings(

db.add(customer_provisioning_default_settings)
await db.commit()
await db.refresh(customer_provisioning_default_settings)
return CustomerProvisioningDefaultSettingsResponse(
message="Customer Provisioning Default Settings created successfully",
success=True,
Expand Down
3 changes: 2 additions & 1 deletion backend/app/customer_provisioning/services/provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,11 @@ async def provision_haproxy(
"""
logger.info(f"Provisioning HAProxy {request}")
api_endpoint = await get_connector_attribute(
connector_id=16,
connector_id=15,
column_name="connector_url",
session=session,
)
logger.info(f"HAProxy API endpoint: {api_endpoint}")
# Send the POST request to the Wazuh worker
response = requests.post(
url=f"{api_endpoint}/provision_worker/haproxy",
Expand Down
122 changes: 122 additions & 0 deletions backend/app/integrations/scoutsuite/routes/scoutsuite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import os

from fastapi import APIRouter
from fastapi import BackgroundTasks
from fastapi import HTTPException
from loguru import logger

from app.integrations.scoutsuite.schema.scoutsuite import (
AvailableScoutSuiteReportsResponse,
)
from app.integrations.scoutsuite.schema.scoutsuite import AWSScoutSuiteReportRequest
from app.integrations.scoutsuite.schema.scoutsuite import ScoutSuiteReportOptions
from app.integrations.scoutsuite.schema.scoutsuite import (
ScoutSuiteReportOptionsResponse,
)
from app.integrations.scoutsuite.schema.scoutsuite import ScoutSuiteReportResponse
from app.integrations.scoutsuite.services.scoutsuite import (
generate_aws_report_background,
)

integration_scoutsuite_router = APIRouter()


@integration_scoutsuite_router.get(
"/report-generation-options",
response_model=ScoutSuiteReportOptionsResponse,
description="Get the available report generation options.",
)
async def get_report_generation_options():
"""
Retrieves the available report generation options for ScoutSuite.
Returns:
ScoutSuiteReportOptionsResponse: The response containing the available report generation options.
"""
return ScoutSuiteReportOptionsResponse(
options=[ScoutSuiteReportOptions.aws, ScoutSuiteReportOptions.azure, ScoutSuiteReportOptions.gcp],
success=True,
message="ScoutSuite Report generation options retrieved successfully",
)


@integration_scoutsuite_router.get(
"/available-reports",
response_model=AvailableScoutSuiteReportsResponse,
description="Get the available ScoutSuite reports.",
)
async def get_available_reports():
"""
List all the `.html` files from the `scoutsuite-report` directory
Returns:
AvailableScoutSuiteReportsResponse: The response containing the list of available ScoutSuite reports.
Raises:
HTTPException: If the directory does not exist.
"""
directory = "scoutsuite-report"
full_path = os.path.abspath(directory)

logger.info(f"Checking directory: {full_path}")

if not os.path.exists(directory):
raise HTTPException(status_code=404, detail="Directory does not exist")

files = os.listdir(directory)
html_files = [file for file in files if file.endswith(".html")]

return AvailableScoutSuiteReportsResponse(
available_reports=html_files,
success=True,
message="Available ScoutSuite reports retrieved successfully",
)


@integration_scoutsuite_router.post(
"/generate-aws-report",
response_model=ScoutSuiteReportResponse,
)
async def generate_aws_report(
background_tasks: BackgroundTasks,
request: AWSScoutSuiteReportRequest,
):
"""
Endpoint to generate an AWS ScoutSuite report.
Args:
background_tasks (BackgroundTasks): The background tasks object.
request (AWSScoutSuiteReportRequest): The request object.
session (AsyncSession): The async session object for database operations.
"""
background_tasks.add_task(generate_aws_report_background, request)
return ScoutSuiteReportResponse(
success=True,
message="AWS ScoutSuite report generation started successfully. This will take a few minutes to complete. Check back in shortly.",
)


@integration_scoutsuite_router.delete(
"/delete-report/{report_name}",
response_model=ScoutSuiteReportResponse,
)
async def delete_report(
report_name: str,
):
"""
Endpoint to delete a ScoutSuite report.
Args:
report_name (str): The name of the report to delete.
"""
report_base_name = os.path.splitext(report_name)[0]
report_file_path = f"scoutsuite-report/{report_name}"
exceptions_file_path = f"scoutsuite-report/scoutsuite-results/scoutsuite_exceptions_{report_base_name}.js"
results_file_path = f"scoutsuite-report/scoutsuite-results/scoutsuite_results_{report_base_name}.js"

files_to_delete = [report_file_path, exceptions_file_path, results_file_path]

for file_path in files_to_delete:
if os.path.exists(file_path):
os.remove(file_path)

return ScoutSuiteReportResponse(success=True, message=f"Report {report_name} and associated files deleted successfully")
48 changes: 48 additions & 0 deletions backend/app/integrations/scoutsuite/schema/scoutsuite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from enum import Enum
from typing import List

from fastapi import HTTPException
from pydantic import BaseModel
from pydantic import Field
from pydantic import root_validator


class ScoutSuiteReportOptions(str, Enum):
aws = "aws"
azure = "azure"
gcp = "gcp"


class ScoutSuiteReportOptionsResponse(BaseModel):
options: List[ScoutSuiteReportOptions] = Field(
...,
description="The available report generation options",
example=["aws", "azure", "gcp"],
)
success: bool
message: str


class AWSScoutSuiteReportRequest(BaseModel):
report_type: str = Field(..., description="The type of report to generate", example="aws")
access_key_id: str = Field(..., description="The AWS access key ID", example="AKIAIOSFODNN7EXAMPLE")
secret_access_key: str = Field(..., description="The AWS secret access key", example="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
report_name: str = Field(..., description="The name of the report", example="aws-report")

@root_validator
def validate_report_type(cls, values):
report_type = values.get("report_type")
if report_type != ScoutSuiteReportOptions.aws:
raise HTTPException(status_code=400, detail="Invalid report type.")
return values


class ScoutSuiteReportResponse(BaseModel):
success: bool
message: str


class AvailableScoutSuiteReportsResponse(BaseModel):
success: bool
message: str
available_reports: List[str]
50 changes: 50 additions & 0 deletions backend/app/integrations/scoutsuite/services/scoutsuite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import asyncio
import subprocess
from concurrent.futures import ThreadPoolExecutor

from loguru import logger

from app.integrations.scoutsuite.schema.scoutsuite import AWSScoutSuiteReportRequest


async def generate_aws_report_background(request: AWSScoutSuiteReportRequest):
logger.info("Generating AWS ScoutSuite report in the background")

command = construct_aws_command(request)
await run_command_in_background(command)


def construct_aws_command(request: AWSScoutSuiteReportRequest):
"""Construct the scout command."""
return [
"scout",
"aws",
"--access-key-id",
request.access_key_id,
"--secret-access-key",
request.secret_access_key,
"--report-name",
request.report_name,
"--force",
"--no-browser",
]


def run_command(command):
"""Run the command and handle the output."""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()

if process.returncode != 0:
logger.error(f"ScoutSuite report generation failed: {stderr.decode()}")
return None

logger.info("ScoutSuite report generated successfully")
return None


async def run_command_in_background(command):
"""Run the command in a separate thread."""
with ThreadPoolExecutor() as executor:
loop = asyncio.get_event_loop()
await loop.run_in_executor(executor, lambda: run_command(command))
13 changes: 13 additions & 0 deletions backend/app/routers/scoutsuite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import APIRouter

from app.integrations.scoutsuite.routes.scoutsuite import integration_scoutsuite_router

# Instantiate the APIRouter
router = APIRouter()

# Include the ScoutSuite related routes
router.include_router(
integration_scoutsuite_router,
prefix="/scoutsuite",
tags=["ScoutSuite"],
)
10 changes: 10 additions & 0 deletions backend/copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from fastapi import HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from loguru import logger

from app.auth.utils import AuthHandler
Expand Down Expand Up @@ -55,6 +56,7 @@
from app.routers import office365
from app.routers import sap_siem
from app.routers import scheduler
from app.routers import scoutsuite
from app.routers import shuffle
from app.routers import smtp
from app.routers import stack_provisioning
Expand Down Expand Up @@ -141,6 +143,7 @@
api_router.include_router(carbonblack.router)
api_router.include_router(network_connectors.router)
api_router.include_router(crowdstrike.router)
api_router.include_router(scoutsuite.router)

# Include the APIRouter in the FastAPI app
app.include_router(api_router)
Expand Down Expand Up @@ -168,6 +171,13 @@ async def init_db():
scheduler.start()


# Create `scoutsuite-report` directory if it doesnt exist
if not os.path.exists("scoutsuite-report"):
os.makedirs("scoutsuite-report")

app.mount("/scoutsuite-report", StaticFiles(directory="scoutsuite-report"), name="scoutsuite-report")


@app.get("/")
def hello():
return {"message": "CoPilot - We Made It!"}
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rich==13.6.0
rsa==4.9
ScoutSuite==5.14.0
setuptools==65.5.0
simplejson==3.19.1
six==1.16.0
Expand Down
1 change: 0 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
"version": "1.0.0",
"private": true,
"type": "module",
"overrides": {
"secure-ls": {
"crypto-js": "^4.2.0"
}
},
"scripts": {
"dev": "vite --host 0.0.0.0",
"dev:debug": "DEBUG=vite:* vite",
Expand Down Expand Up @@ -120,10 +115,15 @@
"vitest": "^1.6.0",
"vue-tsc": "^2.0.19"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.18.0"
},
"overrides": {
"secure-ls": {
"crypto-js": "^4.2.0"
}
},
"engines": {
"node": ">=18.0.0"
}
}
Loading

0 comments on commit 6f75c39

Please sign in to comment.