-
{column.links.map((link) => (
- - {link.label} + {link.href.startsWith("/") ? ( + {link.label} + ) : link.href.startsWith("#") ? ( + {link.label} + ) : ( + {link.label} + )} ))}
diff --git a/backend-api/alembic/env.py b/backend-api/alembic/env.py
index e7ab362f..b94188d8 100644
--- a/backend-api/alembic/env.py
+++ b/backend-api/alembic/env.py
@@ -19,6 +19,7 @@
from app.models.azure_connection import AzureConnection # noqa
from app.models.gcp_connection import GCPConnection # noqa
from app.models.aws_connection import AWSConnection # noqa
+from app.models.user_settings import UserSettings # noqa
from app.core.config import get_settings
# this is the Alembic Config object, which provides
diff --git a/backend-api/alembic/versions/j1k2l3m4n567_add_contact_us_tables.py b/backend-api/alembic/versions/j1k2l3m4n567_add_contact_us_tables.py
index 2049f04d..1fcf3085 100644
--- a/backend-api/alembic/versions/j1k2l3m4n567_add_contact_us_tables.py
+++ b/backend-api/alembic/versions/j1k2l3m4n567_add_contact_us_tables.py
@@ -1,7 +1,7 @@
"""add contact us tables
Revision ID: j1k2l3m4n567
-Revises: h1i2j3k4l567
+Revises: j2k3l4m5n678
Create Date: 2026-01-20
"""
@@ -14,7 +14,7 @@
# revision identifiers, used by Alembic.
revision: str = "j1k2l3m4n567"
-down_revision: Union[str, Sequence[str], None] = "h1i2j3k4l567"
+down_revision: Union[str, Sequence[str], None] = "j2k3l4m5n678"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
diff --git a/backend-api/alembic/versions/j2k3l4m5n678_add_user_settings_table.py b/backend-api/alembic/versions/j2k3l4m5n678_add_user_settings_table.py
new file mode 100644
index 00000000..b6133bd0
--- /dev/null
+++ b/backend-api/alembic/versions/j2k3l4m5n678_add_user_settings_table.py
@@ -0,0 +1,60 @@
+"""Add user_settings table.
+
+Revision ID: j2k3l4m5n678
+Revises: h1i2j3k4l567
+Create Date: 2026-01-18
+"""
+
+from typing import Sequence, Union
+
+import sqlalchemy as sa
+from alembic import op
+
+
+# revision identifiers, used by Alembic.
+revision: str = "j2k3l4m5n678"
+down_revision: Union[str, Sequence[str], None] = "h1i2j3k4l567"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ op.create_table(
+ "user_settings",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("user_id", sa.Integer(), nullable=False),
+ sa.Column(
+ "confirm_delete_enabled",
+ sa.Boolean(),
+ nullable=False,
+ server_default=sa.text("true"),
+ ),
+ sa.Column(
+ "created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False
+ ),
+ sa.Column(
+ "updated_at",
+ sa.DateTime(),
+ server_default=sa.text("now()"),
+ onupdate=sa.text("now()"),
+ nullable=False,
+ ),
+ sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("user_id", name="uq_user_settings_user_id"),
+ )
+
+ op.create_index(
+ op.f("ix_user_settings_user_id"),
+ "user_settings",
+ ["user_id"],
+ unique=True,
+ )
+
+
+def downgrade() -> None:
+ """Downgrade schema."""
+ op.drop_index(op.f("ix_user_settings_user_id"), table_name="user_settings")
+ op.drop_table("user_settings")
+
diff --git a/backend-api/app/Contact-ER-Diagram.jpg b/backend-api/app/Contact-ER-Diagram.jpg
new file mode 100644
index 00000000..908e195d
Binary files /dev/null and b/backend-api/app/Contact-ER-Diagram.jpg differ
diff --git a/backend-api/app/api/_init_.py b/backend-api/app/api/_init_.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/backend-api/app/api/v1/contact.py b/backend-api/app/api/v1/contact.py
index 4af0ae7c..5640041b 100644
--- a/backend-api/app/api/v1/contact.py
+++ b/backend-api/app/api/v1/contact.py
@@ -136,26 +136,38 @@ def track_change(field: str, old: str | None, new: str | None) -> None:
if "status" in payload.model_fields_set:
if payload.status is None:
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="status cannot be null")
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="status cannot be null",
+ )
track_change("status", submission.status, payload.status)
new_status = payload.status.lower()
submission.status = payload.status
- if new_status == "resolved":
+ # If status is set to resolved and resolved_at wasn't explicitly provided,
+ # set it automatically.
+ if new_status == "resolved" and "resolved_at" not in payload.model_fields_set:
submission.resolved_at = datetime.utcnow()
- elif submission.resolved_at is not None:
+ # If status changes away from resolved, clear resolved_at unless caller
+ # is explicitly setting it.
+ elif (
+ new_status != "resolved"
+ and submission.resolved_at is not None
+ and "resolved_at" not in payload.model_fields_set
+ ):
submission.resolved_at = None
if "priority" in payload.model_fields_set:
if payload.priority is None:
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="priority cannot be null")
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="priority cannot be null",
+ )
track_change("priority", submission.priority, payload.priority)
submission.priority = payload.priority
if "assigned_to" in payload.model_fields_set:
if payload.assigned_to is not None:
- user_result = await db.execute(
- select(User).where(User.id == payload.assigned_to)
- )
+ user_result = await db.execute(select(User).where(User.id == payload.assigned_to))
assigned_user = user_result.scalar_one_or_none()
if not assigned_user:
raise HTTPException(
diff --git a/backend-api/app/api/v1/router.py b/backend-api/app/api/v1/router.py
index 0a06f296..c648004a 100644
--- a/backend-api/app/api/v1/router.py
+++ b/backend-api/app/api/v1/router.py
@@ -1,5 +1,15 @@
from fastapi import APIRouter
-from app.api.v1 import auth, test, evidence, m365_connections, scans, benchmarks, platforms, contact
+from app.api.v1 import (
+ auth,
+ benchmarks,
+ contact,
+ evidence,
+ m365_connections,
+ platforms,
+ scans,
+ settings,
+ test,
+)
api_router = APIRouter()
@@ -26,3 +36,6 @@
# Contact routes
api_router.include_router(contact.router)
+
+# User settings routes
+api_router.include_router(settings.router)
diff --git a/backend-api/app/api/v1/scans.py b/backend-api/app/api/v1/scans.py
index 5282d064..f48c7933 100644
--- a/backend-api/app/api/v1/scans.py
+++ b/backend-api/app/api/v1/scans.py
@@ -1,7 +1,7 @@
"""Scan API endpoints."""
from fastapi import APIRouter, Depends, HTTPException, status
-from sqlalchemy import select
+from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -184,3 +184,26 @@ async def get_scan_results(
results = await db.execute(query)
return list(results.scalars().all())
+
+
+@router.delete("/{scan_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_scan(
+ scan_id: int,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_async_session),
+) -> None:
+ """Delete a scan (hard delete) and its results."""
+ result = await db.execute(
+ select(Scan).where(Scan.id == scan_id, Scan.user_id == current_user.id)
+ )
+ scan = result.scalar_one_or_none()
+ if not scan:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"Scan {scan_id} not found",
+ )
+
+ # Delete dependent results first (FK is not ON DELETE CASCADE).
+ await db.execute(delete(ScanResult).where(ScanResult.scan_id == scan_id))
+ await db.delete(scan)
+ await db.commit()
diff --git a/backend-api/app/api/v1/settings.py b/backend-api/app/api/v1/settings.py
new file mode 100644
index 00000000..85c508cb
--- /dev/null
+++ b/backend-api/app/api/v1/settings.py
@@ -0,0 +1,56 @@
+"""User settings API endpoints."""
+
+from fastapi import APIRouter, Depends, status
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.core.auth import get_current_user
+from app.db.session import get_async_session
+from app.models.user import User
+from app.models.user_settings import UserSettings
+from app.schemas.user_settings import UserSettingsRead, UserSettingsUpdate
+
+router = APIRouter(prefix="/settings", tags=["Settings"])
+
+
+async def _get_or_create_settings(
+ db: AsyncSession,
+ user_id: int,
+) -> UserSettings:
+ result = await db.execute(select(UserSettings).where(UserSettings.user_id == user_id))
+ settings = result.scalar_one_or_none()
+ if settings:
+ return settings
+
+ settings = UserSettings(user_id=user_id)
+ db.add(settings)
+ await db.commit()
+ await db.refresh(settings)
+ return settings
+
+
+@router.get("/", response_model=UserSettingsRead)
+async def get_my_settings(
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_async_session),
+) -> UserSettings:
+ """Get settings for the current user (creates defaults on first access)."""
+ return await _get_or_create_settings(db, current_user.id)
+
+
+@router.patch("/", response_model=UserSettingsRead)
+async def update_my_settings(
+ update: UserSettingsUpdate,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_async_session),
+) -> UserSettings:
+ """Update settings for the current user."""
+ settings = await _get_or_create_settings(db, current_user.id)
+
+ if update.confirm_delete_enabled is not None:
+ settings.confirm_delete_enabled = update.confirm_delete_enabled
+
+ await db.commit()
+ await db.refresh(settings)
+ return settings
+
diff --git a/backend-api/app/contactus api flow AutoAudit.jpg b/backend-api/app/contactus api flow AutoAudit.jpg
new file mode 100644
index 00000000..6ca9e293
Binary files /dev/null and b/backend-api/app/contactus api flow AutoAudit.jpg differ
diff --git a/backend-api/app/models/__init__.py b/backend-api/app/models/__init__.py
index 004d1684..27c476e4 100644
--- a/backend-api/app/models/__init__.py
+++ b/backend-api/app/models/__init__.py
@@ -11,6 +11,7 @@
from app.models.compliance import Scan
from app.models.evidence_validation import EvidenceValidation
from app.models.contact import ContactSubmission, SubmissionNote, SubmissionHistory
+from app.models.user_settings import UserSettings
__all__ = [
"User",
@@ -27,4 +28,5 @@
"ContactSubmission",
"SubmissionNote",
"SubmissionHistory",
+ "UserSettings",
]
diff --git a/backend-api/app/models/user.py b/backend-api/app/models/user.py
index 211d3689..7db3b99f 100644
--- a/backend-api/app/models/user.py
+++ b/backend-api/app/models/user.py
@@ -13,6 +13,7 @@
from app.models.m365_connection import M365Connection
from app.models.contact import ContactSubmission, SubmissionHistory, SubmissionNote
from app.models.oauth_account import OAuthAccount
+ from app.models.user_settings import UserSettings
class Role(str, Enum):
@@ -49,6 +50,12 @@ class User(SQLAlchemyBaseUserTable[int], Base):
cascade="all, delete-orphan",
lazy="selectin",
)
+ settings: Mapped["UserSettings"] = relationship(
+ back_populates="user",
+ uselist=False,
+ cascade="all, delete-orphan",
+ lazy="selectin",
+ )
m365_connections: Mapped[list["M365Connection"]] = relationship(
back_populates="user"
)
diff --git a/backend-api/app/models/user_settings.py b/backend-api/app/models/user_settings.py
new file mode 100644
index 00000000..acec6de4
--- /dev/null
+++ b/backend-api/app/models/user_settings.py
@@ -0,0 +1,42 @@
+from datetime import datetime
+
+from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, text
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+from sqlalchemy.sql import func
+
+from app.db.base import Base
+
+
+class UserSettings(Base):
+ """Per-user application settings.
+
+ One row per user (enforced via unique constraint on user_id).
+ """
+
+ __tablename__ = "user_settings"
+
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ user_id: Mapped[int] = mapped_column(
+ Integer,
+ ForeignKey("user.id", ondelete="CASCADE"),
+ nullable=False,
+ unique=True,
+ index=True,
+ )
+
+ # UI preference: show confirmation dialog before destructive deletes.
+ confirm_delete_enabled: Mapped[bool] = mapped_column(
+ Boolean,
+ nullable=False,
+ server_default=text("true"),
+ )
+
+ created_at: Mapped[datetime] = mapped_column(
+ DateTime, nullable=False, server_default=func.now()
+ )
+ updated_at: Mapped[datetime] = mapped_column(
+ DateTime, nullable=False, server_default=func.now(), onupdate=func.now()
+ )
+
+ user = relationship("User", back_populates="settings")
+
diff --git a/backend-api/app/schemas/user_settings.py b/backend-api/app/schemas/user_settings.py
new file mode 100644
index 00000000..6bcb3e0d
--- /dev/null
+++ b/backend-api/app/schemas/user_settings.py
@@ -0,0 +1,25 @@
+"""Pydantic schemas for per-user settings."""
+
+from datetime import datetime
+
+from pydantic import BaseModel
+
+
+class UserSettingsRead(BaseModel):
+ """Schema for reading user settings."""
+
+ id: int
+ user_id: int
+ confirm_delete_enabled: bool
+ created_at: datetime
+ updated_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class UserSettingsUpdate(BaseModel):
+ """Schema for updating user settings."""
+
+ confirm_delete_enabled: bool | None = None
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 67c0de76..c0edaff0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -174,6 +174,7 @@ services:
powershell-service:
profiles: ["powershell", "all"]
+ platform: linux/amd64
build:
context: ./engine/powershell
dockerfile: Dockerfile
diff --git a/engine/collectors/entra/applications/apps_and_services_settings.py b/engine/collectors/entra/applications/apps_and_services_settings.py
index 43b00837..fed92ba9 100644
--- a/engine/collectors/entra/applications/apps_and_services_settings.py
+++ b/engine/collectors/entra/applications/apps_and_services_settings.py
@@ -5,7 +5,7 @@
Connection Method: Microsoft Graph API
Required Scopes: OrgSettings-AppsAndServices.Read.All
-Graph Endpoint: /admin/serviceAnnouncement/messages (or organization settings)
+Graph Endpoint: /admin/appsAndServices (beta)
"""
from typing import Any
@@ -27,18 +27,60 @@ async def collect(self, client: GraphClient) -> dict[str, Any]:
Returns:
Dict containing:
- apps_and_services_settings: Apps and services configuration
- - user_owned_apps_enabled: Whether users can own apps
+ - is_office_store_enabled: Whether users can access the Office Store
+ - is_app_and_services_trial_enabled: Whether users can start trials
+ - user_owned_apps_enabled: Back-compat derived signal (best-effort)
"""
- # Get organization settings for apps and services
- # This uses the admin/microsoft365Apps settings endpoint
+ # Prefer the documented Apps & Services admin settings endpoint.
+ # Keep a fallback to the older path for compatibility with older tenants.
+ collector_error: str | None = None
try:
- settings = await client.get("/admin/microsoft365Apps/settings", beta=True)
- except Exception:
- # Fallback if endpoint not available
- settings = {}
+ resp = await client.get("/admin/appsAndServices", beta=True)
+ raw_settings: dict[str, Any] | None = None
+
+ # Handle a few plausible response shapes defensively.
+ if isinstance(resp, dict):
+ if isinstance(resp.get("settings"), dict):
+ raw_settings = resp.get("settings")
+ else:
+ value = resp.get("value")
+ if isinstance(value, dict) and isinstance(value.get("settings"), dict):
+ raw_settings = value.get("settings")
+ elif isinstance(value, list) and value and isinstance(value[0], dict):
+ if isinstance(value[0].get("settings"), dict):
+ raw_settings = value[0].get("settings")
+
+ settings = raw_settings if raw_settings is not None else (resp or {})
+ except Exception as exc:
+ collector_error = str(exc)
+ try:
+ settings = await client.get("/admin/microsoft365Apps/settings", beta=True)
+ except Exception as exc2:
+ collector_error = f"{collector_error} | fallback_error={exc2}"
+ settings = {}
+
+ office_store_enabled = (
+ settings.get("isOfficeStoreEnabled") if isinstance(settings, dict) else None
+ )
+ trial_enabled = (
+ settings.get("isAppAndServicesTrialEnabled") if isinstance(settings, dict) else None
+ )
+
+ # Best-effort derived signal used by the old policy implementation.
+ derived_user_owned_apps_enabled: bool | None
+ if isinstance(settings, dict) and "isUserAppsAndServicesEnabled" in settings:
+ derived_user_owned_apps_enabled = settings.get("isUserAppsAndServicesEnabled")
+ elif office_store_enabled is False and trial_enabled is False:
+ derived_user_owned_apps_enabled = False
+ elif office_store_enabled is True or trial_enabled is True:
+ derived_user_owned_apps_enabled = True
+ else:
+ derived_user_owned_apps_enabled = None
return {
"apps_and_services_settings": settings,
- "user_owned_apps_enabled": settings.get("isUserAppsAndServicesEnabled"),
- "is_office_store_enabled": settings.get("isOfficeStoreEnabled"),
+ "is_office_store_enabled": office_store_enabled,
+ "is_app_and_services_trial_enabled": trial_enabled,
+ "user_owned_apps_enabled": derived_user_owned_apps_enabled,
+ "collector_error": collector_error,
}
diff --git a/engine/collectors/entra/applications/forms_settings.py b/engine/collectors/entra/applications/forms_settings.py
index 56452289..0e8291d1 100644
--- a/engine/collectors/entra/applications/forms_settings.py
+++ b/engine/collectors/entra/applications/forms_settings.py
@@ -31,29 +31,114 @@ async def collect(self, client: GraphClient) -> dict[str, Any]:
- external_sharing_enabled: External sharing status
"""
# Get Microsoft Forms admin settings
+ collector_error: str | None = None
+ resp: dict[str, Any] = {}
+ entry: dict[str, Any] | None = None
try:
- settings = await client.get("/admin/forms/settings", beta=True)
- except Exception:
+ resp = await client.get("/admin/forms/settings", beta=True)
+ raw_settings: dict[str, Any] | None = None
+
+ # Handle a few plausible response shapes defensively.
+ if isinstance(resp, dict):
+ if isinstance(resp.get("settings"), dict):
+ raw_settings = resp.get("settings")
+ elif isinstance(resp.get("formsSettings"), dict):
+ raw_settings = resp.get("formsSettings")
+ else:
+ value = resp.get("value")
+ if isinstance(value, dict):
+ entry = value
+ if isinstance(value.get("settings"), dict):
+ raw_settings = value.get("settings")
+ elif isinstance(value.get("formsSettings"), dict):
+ raw_settings = value.get("formsSettings")
+ else:
+ raw_settings = value
+ elif isinstance(value, list) and value and isinstance(value[0], dict):
+ entry = value[0]
+ if isinstance(entry.get("settings"), dict):
+ raw_settings = entry.get("settings")
+ elif isinstance(entry.get("formsSettings"), dict):
+ raw_settings = entry.get("formsSettings")
+ else:
+ raw_settings = entry
+
+ settings = raw_settings if raw_settings is not None else (resp or {})
+ except Exception as exc:
+ collector_error = str(exc)
settings = {}
+ def _get_setting_value(*keys: str) -> Any:
+ for source in (settings, entry, resp):
+ if isinstance(source, dict):
+ for key in keys:
+ if key in source:
+ return source.get(key)
+ return None
+
+ def _normalize_bool(value: Any) -> bool | None:
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ normalized = value.strip().lower()
+ if normalized in {"on", "enabled", "true", "yes"}:
+ return True
+ if normalized in {"off", "disabled", "false", "no"}:
+ return False
+ return None
+
+ internal_phishing_protection_enabled = _normalize_bool(
+ _get_setting_value(
+ "isInternalPhishingProtectionEnabled",
+ "internalPhishingProtectionEnabled",
+ "isInOrgFormsPhishingScanEnabled",
+ "inOrgFormsPhishingScanEnabled",
+ "FormsPhishingProtection",
+ "formsPhishingProtection",
+ "phishingProtection",
+ )
+ )
+
return {
"forms_settings": settings,
- "internal_phishing_protection_enabled": settings.get(
- "isInternalPhishingProtectionEnabled"
+ "internal_phishing_protection_enabled": internal_phishing_protection_enabled,
+ "external_sharing_enabled": _normalize_bool(
+ _get_setting_value("isExternalSharingEnabled", "externalSharingEnabled")
+ ),
+ "external_send_form_enabled": _normalize_bool(
+ _get_setting_value("isExternalSendFormEnabled", "externalSendFormEnabled")
+ ),
+ "external_share_collaborating_enabled": _normalize_bool(
+ _get_setting_value(
+ "isExternalShareCollaborationEnabled",
+ "isExternalShareCollaboratingEnabled",
+ "externalShareCollaboratingEnabled",
+ )
),
- "external_sharing_enabled": settings.get("isExternalSharingEnabled"),
- "external_send_form_enabled": settings.get("isExternalSendFormEnabled"),
- "external_share_collaborating_enabled": settings.get(
- "isExternalShareCollaborationEnabled"
+ "external_share_template_enabled": _normalize_bool(
+ _get_setting_value(
+ "isExternalShareTemplateEnabled",
+ "externalShareTemplateEnabled",
+ )
),
- "external_share_template_enabled": settings.get(
- "isExternalShareTemplateEnabled"
+ "external_share_result_enabled": _normalize_bool(
+ _get_setting_value(
+ "isExternalShareResultEnabled",
+ "externalShareResultEnabled",
+ )
),
- "external_share_result_enabled": settings.get(
- "isExternalShareResultEnabled"
+ "bing_search_enabled": _normalize_bool(
+ _get_setting_value(
+ "isBingSearchEnabled",
+ "bingSearchEnabled",
+ "isBingImageSearchEnabled",
+ )
),
- "bing_search_enabled": settings.get("isBingSearchEnabled"),
- "record_identity_by_default_enabled": settings.get(
- "isRecordIdentityByDefaultEnabled"
+ "record_identity_by_default_enabled": _normalize_bool(
+ _get_setting_value(
+ "isRecordIdentityByDefaultEnabled",
+ "recordIdentityByDefaultEnabled",
+ )
),
+ "collector_error": collector_error,
}
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_10.rego b/engine/legacy/rules-gcp/CIS_GCP_2_10.rego
index e36e0f88..af9504de 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_10.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_10.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_10"
title := "Ensure That the Log Metric Filter and Alerts Exist for Cloud Storage IAM Permission Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `1. List the log metrics:
gcloud logging metrics list --format json
2. Ensure that the output contains at least one metric with the filter set to:
@@ -27,7 +25,6 @@ remediation := `Create the prescribed Log Metric:
Create the prescribed alert policy:
• Use the command: gcloud alpha monitoring policies create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not gcs_iam_metric_exists
v := sprintf("Project %q: Missing metric for Storage IAM permission changes", [input.project_id])
@@ -51,8 +48,4 @@ gcs_iam_alert_enabled_for_user_metric {
contains(filt, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_11.rego b/engine/legacy/rules-gcp/CIS_GCP_2_11.rego
index a39c1d85..657c360b 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_11.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_11.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_11"
title := "Ensure That the Log Metric Filter and Alerts Exist for SQL Instance Configuration Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `1. List the log metrics:
gcloud logging metrics list --format json
2. Ensure that the output contains at least one metric with the filter set to
@@ -28,7 +26,6 @@ Create the prescribed alert policy:
• Reference for command usage:
https://cloud.google.com/sdk/gcloud/reference/alpha/monitoring/policies/create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not cloudsql_metric_exists
v := sprintf("Project %q: Missing metric for Cloud SQL instance updates", [input.project_id])
@@ -52,8 +49,4 @@ cloudsql_alert_enabled_for_user_metric {
contains(filt, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_4.rego b/engine/legacy/rules-gcp/CIS_GCP_2_4.rego
index 11c6c65f..d94022a8 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_4.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_4.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_4"
title := "Ensure Log Metric Filter and Alerts Exist for Project Ownership Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `1. List the log metrics:
gcloud logging metrics list --format json
2. Ensure that the output contains at least one metric with filter set to:
@@ -34,7 +32,6 @@ Create prescribed Alert Policy
• Reference for Command Usage:
https://cloud.google.com/sdk/gcloud/reference/alpha/monitoring/policies/create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not project_owner_metric_exists
v := sprintf("Project %q: Missing logs-based metric for project ownership changes", [input.project_id])
@@ -59,8 +56,4 @@ alert_policy_referencing_user_metric_enabled {
contains(c.conditionThreshold.filter, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_5.rego b/engine/legacy/rules-gcp/CIS_GCP_2_5.rego
index bb86ced6..4879c8db 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_5.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_5.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_5"
title := "Ensure That the Log Metric Filter and Alerts Exist for Audit Configuration Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `1. List the log metrics:
gcloud beta logging metrics list --format json
@@ -32,7 +30,6 @@ Create prescribed Alert Policy
• Reference for command usage:
https://cloud.google.com/sdk/gcloud/reference/alpha/monitoring/policies/create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not audit_config_metric_exists
v := sprintf("Project %q: Missing logs-based metric for audit config changes (SetIamPolicy)", [input.project_id])
@@ -57,8 +54,4 @@ alert_policy_referencing_user_metric_enabled {
contains(filt, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_6.rego b/engine/legacy/rules-gcp/CIS_GCP_2_6.rego
index 08f210cb..5d6e14b5 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_6.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_6.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_6"
title := "Ensure That the Log Metric Filter and Alerts Exist for Custom Role Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `Ensure that the prescribed log metric is present:
1. List the log metrics:
gcloud logging metrics list --format json
@@ -31,7 +29,6 @@ remediation := `Create the prescribed Log Metric:
Create the prescribed Alert Policy:
• Use the command: gcloud alpha monitoring policies create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not custom_role_metric_exists
v := sprintf("Project %q: Missing logs-based metric for IAM custom role changes", [input.project_id])
@@ -58,8 +55,4 @@ alert_policy_referencing_user_metric_enabled {
contains(filt, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_7.rego b/engine/legacy/rules-gcp/CIS_GCP_2_7.rego
index 49a5452e..4df10b69 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_7.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_7.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_7"
title := "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Firewall Rule Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `1. List the log metrics:
gcloud logging metrics list --format json
2. Ensure that the output contains at least one metric with the filter set to:
@@ -29,7 +27,6 @@ remediation := `Create the prescribed Log Metric
Create the prescribed alert policy:
• Use the command: gcloud alpha monitoring policies create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not firewall_metric_exists
v := sprintf("Project %q: Missing metric for firewall rule changes", [input.project_id])
@@ -58,8 +55,4 @@ firewall_alert_enabled_for_user_metric {
contains(filt, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_8.rego b/engine/legacy/rules-gcp/CIS_GCP_2_8.rego
index fc68b98c..3aae06d1 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_8.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_8.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_8"
title := "Ensure Log Metric Filter and Alerts Exist for VPC Route Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `1. List the log metrics:
gcloud logging metrics list --format json
2. Ensure that the output contains at least one metric with the filter set to:
@@ -28,7 +26,6 @@ remediation := `Create the prescribed Log Metric:
Create the prescribed the alert policy:
• Use the command: gcloud alpha monitoring policies create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not route_metric_exists
v := sprintf("Project %q: Missing logs-based metric for VPC route/peering changes", [input.project_id])
@@ -55,8 +52,4 @@ alert_policy_referencing_user_metric_enabled {
contains(c.conditionThreshold.filter, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/legacy/rules-gcp/CIS_GCP_2_9.rego b/engine/legacy/rules-gcp/CIS_GCP_2_9.rego
index 05dc7939..2c9e381d 100644
--- a/engine/legacy/rules-gcp/CIS_GCP_2_9.rego
+++ b/engine/legacy/rules-gcp/CIS_GCP_2_9.rego
@@ -5,8 +5,6 @@ id := "CIS_GCP_2_9"
title := "Ensure Log Metric Filter and Alerts Exist for VPC Network Changes"
policy_group := "Logging and Monitoring"
-<<<<<<< HEAD
-=======
verification := `1. List the log metrics:
gcloud logging metrics list --format json
2. Ensure that the output contains at least one metric with filter set to:
@@ -31,7 +29,6 @@ remediation := `Create the prescribed Log Metric:
Create the prescribed alert policy:
• Use the command: gcloud alpha monitoring policies create`
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
deny := { v |
not vpc_network_metric_exists
v := sprintf("Project %q: Missing logs-based metric for VPC network create/update/delete", [input.project_id])
@@ -54,8 +51,4 @@ alert_policy_referencing_user_metric_enabled {
contains(c.conditionThreshold.filter, "logging.googleapis.com/user/")
}
-<<<<<<< HEAD
-report := H.build_report(deny, id, title, policy_group)
-=======
report := H.build_report(deny, id, title, policy_group, verification, remediation)
->>>>>>> 99b403da (finished final group 2 policy tagging with verification and remediaton)
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.4_user_owned_apps_restricted.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.4_user_owned_apps_restricted.rego
index 0b0308a9..c55b2cb9 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.4_user_owned_apps_restricted.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.4_user_owned_apps_restricted.rego
@@ -15,7 +15,7 @@
# severity: medium
# service: EntraID
# requires_permissions:
-# - Policy.Read.All
+# - OrgSettings-AppsAndServices.Read.All
package cis.microsoft_365_foundations.v6_0_0.control_1_3_4
@@ -27,25 +27,45 @@ default result := {
"details": {}
}
+has_modern := true if {
+ input.is_office_store_enabled != null
+ input.is_app_and_services_trial_enabled != null
+} else := false if { true }
+
+unknown := true if { input.user_owned_apps_enabled == null; input.is_office_store_enabled == null } else := true if { input.user_owned_apps_enabled == null; input.is_app_and_services_trial_enabled == null } else := false if { true }
+
+compliant := true if {
+ has_modern
+ input.is_office_store_enabled == false
+ input.is_app_and_services_trial_enabled == false
+} else := true if {
+ not has_modern
+ input.user_owned_apps_enabled == false
+} else := false if { true }
+
result := output if {
- enabled := input.user_owned_apps_enabled
+ office := input.is_office_store_enabled
+ trial := input.is_app_and_services_trial_enabled
+ legacy_enabled := input.user_owned_apps_enabled
output := {
# CIS intent: restricted => disabled
- "compliant": enabled == false,
- "message": generate_message(enabled),
- "affected_resources": generate_affected(enabled),
+ "compliant": compliant,
+ "message": generate_message(compliant, unknown),
+ "affected_resources": generate_affected(compliant, unknown),
"details": {
- "user_owned_apps_enabled": enabled,
- "is_office_store_enabled": input.is_office_store_enabled
+ "is_office_store_enabled": office,
+ "is_app_and_services_trial_enabled": trial,
+ "user_owned_apps_enabled": legacy_enabled,
+ "collector_error": input.collector_error
}
}
}
-generate_message(true) := "User owned apps and services are not restricted (enabled)"
-generate_message(false) := "User owned apps and services are restricted (disabled)"
-generate_message(null) := "Unable to determine whether user owned apps and services are restricted"
+generate_message(true, false) := "User owned apps and services are restricted (disabled)"
+generate_message(false, false) := "User owned apps and services are not restricted (enabled)"
+generate_message(_, true) := "Unable to determine whether user owned apps and services are restricted"
-generate_affected(false) := []
-generate_affected(true) := ["User owned apps and services are enabled"]
-generate_affected(null) := ["User owned apps and services setting unknown"]
+generate_affected(true, false) := []
+generate_affected(false, false) := ["User owned apps and services are enabled"]
+generate_affected(_, true) := ["User owned apps and services setting unknown"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.5_forms_internal_phishing_protection.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.5_forms_internal_phishing_protection.rego
index 17342aa2..09d2282a 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.5_forms_internal_phishing_protection.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/1.3.5_forms_internal_phishing_protection.rego
@@ -42,7 +42,8 @@ result := output if {
"external_share_template_enabled": input.external_share_template_enabled,
"external_share_result_enabled": input.external_share_result_enabled,
"bing_search_enabled": input.bing_search_enabled,
- "record_identity_by_default_enabled": input.record_identity_by_default_enabled
+ "record_identity_by_default_enabled": input.record_identity_by_default_enabled,
+ "collector_error": input.collector_error
}
}
}
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.11_Comprehensive_Attachment_Filtering_Applied.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.11_Comprehensive_Attachment_Filtering_Applied.rego
index 0986dc9c..5514ac7f 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.11_Comprehensive_Attachment_Filtering_Applied.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.11_Comprehensive_Attachment_Filtering_Applied.rego
@@ -22,6 +22,8 @@ package cis.microsoft_365_foundations.v6_0_0.control_2_1_11
import rego.v1
+default result := {"compliant": false, "message": "Evaluation failed"}
+
attach_exts := [
"7z","a3x","ace","ade","adp","ani","app","appinstaller","applescript","application",
"appref-ms","appx","appxbundle","arj","asd","asx","bas","bat","bgi","bz2","cab",
@@ -42,69 +44,38 @@ attach_exts := [
passing_value := 0.9
-missing_exts[policy_identity] = missing if {
- policy := input.malware_filter_policies[_]
- policy_identity := policy.Identity
-
- missing := [ext |
- ext := attach_exts[_]
- not ext in policy.FileTypes
- ]
-}
-
-is_compliant[policy_identity] if {
- policy := input.malware_filter_policies[_]
- policy_identity := policy.Identity
-
- missing := missing_exts[policy_identity]
- fail_threshold := count(attach_exts) * (1 - passing_value)
+policy := object.get(input, "default_policy", null)
+has_policy := policy != null
- count(missing) <= fail_threshold
- policy.EnableFileFilter == true
-}
-
-generate_message(true) := "Attachment filtering policy is correctly configured and enforced"
-generate_message(false) := "Attachment filtering policy is misconfigured or not enforced"
+enable_file_filter := object.get(policy, "EnableFileFilter", object.get(input, "enable_file_filter", null))
+file_types := object.get(policy, "FileTypes", object.get(input, "file_types", []))
-generate_affected_resources(true, _) := []
-
-generate_affected_resources(false, data_input) := [
- pol.Identity |
- pol := data_input.malware_filter_policies[_]
-]
+missing := [ext | ext := attach_exts[_]; not ext in file_types]
+fail_threshold := count(attach_exts) * (1 - passing_value)
+coverage_ok := count(missing) <= fail_threshold
-policies := object.get(input, "malware_filter_policies", [])
-has_policies := count(policies) > 0
-
-non_compliant_policies := [
- pol.Identity |
- pol := policies[_]
- not is_compliant[pol.Identity]
-]
+compliant := true if {
+ has_policy
+ enable_file_filter == true
+ coverage_ok
+} else := false if { true }
result := {
- "compliant": false,
- "message": "No malware filter policies found",
- "affected_resources": ["MalwareFilterPolicy"],
+ "compliant": compliant,
+ "message": generate_message(compliant, has_policy),
+ "affected_resources": generate_affected_resources(compliant, has_policy),
"details": {
- "malware_filter_policies_evaluated": 0,
+ "EnableFileFilter": enable_file_filter,
+ "missing_count": count(missing),
+ "missing_extensions": missing,
"passing_threshold": passing_value,
- "total_known_extensions": count(attach_exts)
+ "known_extensions_count": count(attach_exts)
}
-} if {
- not has_policies
}
-result := {
- "compliant": count(non_compliant_policies) == 0,
- "message": generate_message(count(non_compliant_policies) == 0),
- "affected_resources": generate_affected_resources(count(non_compliant_policies) == 0, input),
- "details": {
- "malware_filter_policies_evaluated": count(policies),
- "non_compliant_policies": non_compliant_policies,
- "passing_threshold": passing_value,
- "total_known_extensions": count(attach_exts)
- }
-} if {
- has_policies
-}
+generate_message(true, _) := "Comprehensive attachment filtering is applied and covers the expected set of extensions."
+generate_message(false, false) := "No malware filter policy (default_policy) was found to evaluate attachment filtering."
+generate_message(false, true) := "Comprehensive attachment filtering is not applied or does not cover enough extensions."
+
+generate_affected_resources(true, _) := []
+generate_affected_resources(false, _) := ["MalwareFilterPolicy"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.12_ConnectionFilter_IPAllowList_not_used.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.12_ConnectionFilter_IPAllowList_not_used.rego
index 582482a6..923d357b 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.12_ConnectionFilter_IPAllowList_not_used.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.12_ConnectionFilter_IPAllowList_not_used.rego
@@ -2,11 +2,12 @@
# title: Ensure the connection filter IP allow list is not used
# description: |
# In Microsoft 365 organizations with Exchange Online mailboxes or standalone
-# Exchange Online Protection organizations without Exchange Online mailboxes,
+# Exchange Online Protection organizations without Exchange Online mailboxes
# connection filtering and the default connection filter policy identify good or
# bad source email servers by IP addresses. The key components of the default connection
# filter policy are IP Allow List, IP Block List and Safe list.
# The recommended state is IP Allow List empty or undefined.
+#
# related_resources:
# - ref: https://www.cisecurity.org/benchmark/microsoft_365
# description: CIS Microsoft 365 Foundations Benchmark
@@ -22,39 +23,44 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_12
-import rego.v1
-
default result := {"compliant": false, "message": "Evaluation failed"}
-policies := object.get(input, "connection_filter_policies", [])
-
-non_compliant_policies := [
- {
- "identity": object.get(p, "Identity", object.get(p, "Name", null)),
- "ip_allow_list": object.get(p, "IPAllowList", [])
- } |
- p := policies[_]
- count(object.get(p, "IPAllowList", [])) > 0
-]
-
-compliant := count(non_compliant_policies) == 0
-
-result := {
- "compliant": compliant,
- "message": messages[compliant],
- "affected_resources": affected_resources[compliant],
- "details": {
- "policies_evaluated": count(policies),
- "non_compliant_policies": non_compliant_policies
- }
+# Required IPAllowList setting
+required_fields := {
+ "IPAllowList": []
}
-messages := {
- true: "IPAllowList is empty for all Exchange Online Hosted Connection Filter policies",
- false: "IPAllowList is not empty for one or more Exchange Online Hosted Connection Filter policies"
-}
+ip_allow_list := object.get(
+ input,
+ "ip_allow_list",
+ object.get(object.get(input, "default_policy", {}), "IPAllowList", null)
+)
+
+ip_allow_list_is_empty := null if {
+ ip_allow_list == null
+} else := true if {
+ ip_allow_list == [] # empty array
+} else := true if {
+ ip_allow_list == {} # empty object
+} else := false
-affected_resources := {
- true: [],
- false: ["HostedConnectionFilterPolicy"]
+result := output if {
+ compliant := ip_allow_list_is_empty == true
+
+ output := {
+ "compliant": compliant,
+ "message": generate_message(ip_allow_list_is_empty),
+ "affected_resources": generate_affected_resources(ip_allow_list_is_empty),
+ "details": {
+ "IPAllowList": ip_allow_list
+ }
+ }
}
+
+generate_message(true) := "IPAllowList is empty or {} in Exchange Online Hosted Connection Filter"
+generate_message(false) := "IPAllowList is not empty or is not {} in Exchange Online Hosted Connection Filter"
+generate_message(null) := "Unable to determine the IPAllowList status in Exchange Online Hosted Connection Filter"
+
+generate_affected_resources(true) := []
+generate_affected_resources(false) := ["HostedConnectionFilterPolicy"]
+generate_affected_resources(null) := ["HostedConnectionFilterPolicy status unknown"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.13_Connection_Filter_SafeList_Off.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.13_Connection_Filter_SafeList_Off.rego
index 6718c79c..e4b3728e 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.13_Connection_Filter_SafeList_Off.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.13_Connection_Filter_SafeList_Off.rego
@@ -24,22 +24,44 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_13
-import rego.v1
-
default result := {"compliant": false, "message": "Evaluation failed"}
-enable_safe_list := object.get(input, "enable_safe_list", false)
-
-result := {
- "compliant": true,
- "message": "EnableSafeList is False for Exchange Online Hosted Connection Filter",
- "affected_resources": [],
- "details": {"EnableSafeList": enable_safe_list}
-} if enable_safe_list == false
-
-result := {
- "compliant": false,
- "message": "EnableSafeList is not False for Exchange Online Hosted Connection Filter",
- "affected_resources": ["HostedConnectionFilterPolicy"],
- "details": {"EnableSafeList": enable_safe_list}
-} if enable_safe_list == true
+# Required EnableSafeList setting
+required_fields := {
+ "EnableSafeList": false
+}
+
+enable_safe_list := object.get(
+ input,
+ "enable_safe_list",
+ object.get(object.get(input, "default_policy", {}), "EnableSafeList", null)
+)
+
+enable_safe_list_is_false := null if {
+ enable_safe_list == null
+} else := true if {
+ enable_safe_list == false # Ensure EnableSafeList is False
+} else := false if {
+ enable_safe_list == true # If EnableSafeList is True, it's non-compliant
+} else := null
+
+result := output if {
+ compliant := enable_safe_list_is_false == true
+
+ output := {
+ "compliant": compliant,
+ "message": generate_message(enable_safe_list_is_false),
+ "affected_resources": generate_affected_resources(enable_safe_list_is_false),
+ "details": {
+ "EnableSafeList": enable_safe_list
+ }
+ }
+}
+
+generate_message(true) := "EnableSafeList is False for Exchange Online Hosted Connection Filter"
+generate_message(false) := "EnableSafeList is not False for Exchange Online Hosted Connection Filter"
+generate_message(null) := "Unable to determine the EnableSafeList status in Exchange Online Hosted Connection Filter"
+
+generate_affected_resources(true) := []
+generate_affected_resources(false) := ["HostedConnectionFilterPolicy"]
+generate_affected_resources(null) := ["HostedConnectionFilterPolicy status unknown"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego
index ecf75ec9..dc99b224 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.14_Inbound_AntiSpam_Policies_DoNot_AllowedDomains.rego
@@ -25,30 +25,46 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_14
-import rego.v1
-
default result := {"compliant": false, "message": "Evaluation failed"}
-allowed_sender_domains := object.get(input, "allowed_sender_domains", [])
+allowed_sender_domains := object.get(
+ input,
+ "allowed_sender_domains",
+ object.get(object.get(input, "default_policy", {}), "AllowedSenderDomains", null)
+)
-allowed_sender_domains_undefined := true if count(allowed_sender_domains) == 0
-allowed_sender_domains_undefined := false if count(allowed_sender_domains) > 0
+allowed_sender_domains_undefined := true if {
+ allowed_sender_domains != null
+ count(allowed_sender_domains) == 0
+}
-result := {
- "compliant": allowed_sender_domains_undefined,
- "message": messages[allowed_sender_domains_undefined],
- "affected_resources": affected_resources[allowed_sender_domains_undefined],
- "details": {
- "allowed_sender_domains": allowed_sender_domains
- }
+allowed_sender_domains_undefined := false if {
+ allowed_sender_domains != null
+ count(allowed_sender_domains) > 0
}
-messages := {
- true: "AllowedSenderDomains is undefined or empty for the policy",
- false: "AllowedSenderDomains is defined for the policy"
+allowed_sender_domains_undefined := null if {
+ allowed_sender_domains == null
}
-affected_resources := {
- true: [],
- false: ["HostedContentFilterPolicy"]
+result := output if {
+ # Ensure all inbound policies pass
+ compliant := allowed_sender_domains_undefined == true
+
+ output := {
+ "compliant": compliant,
+ "message": generate_message(allowed_sender_domains_undefined),
+ "affected_resources": generate_affected_resources(allowed_sender_domains_undefined),
+ "details": {
+ "AllowedSenderDomains": allowed_sender_domains
+ }
+ }
}
+
+generate_message(true) := "AllowedSenderDomains is undefined for the policy"
+generate_message(false) := "AllowedSenderDomains is defined for the policy"
+generate_message(null) := "Unable to determine the AllowedSenderDomains status for the policy"
+
+generate_affected_resources(true) := []
+generate_affected_resources(false) := ["HostedContentFilterPolicy"]
+generate_affected_resources(null) := ["HostedContentFilterPolicy status unknown"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego
index 38b06b8d..b7037015 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.15_Outbound_AntiSpam_MessageLimits_InPlace.rego
@@ -24,121 +24,63 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_15
-import rego.v1
+default result := {"compliant": false, "message": "Evaluation failed"}
-# Expected policy values
-required_policy_fields := {
- "RecipientLimitExternalPerHour": 500,
- "RecipientLimitInternalPerHour": 1000,
- "RecipientLimitPerDay": 1000,
-}
-
-valid_actions := {"BlockUser", "RestrictUser", "Restrict"}
-missing_sentinel := "__missing__"
-
-limits_compliant if {
- policy != null
-
- all_keys_correct := {k | k in required_policy_fields; policy[k] == required_policy_fields[k]}
- count(all_keys_correct) == count(required_policy_fields)
-
- # Action must match one of the valid options
- policy.ActionWhenThresholdReached in valid_actions
-}
-
-limits_compliant := false if {
- policy != null
- some k
- required_policy_fields[k]
- object.get(policy, k, missing_sentinel) == missing_sentinel
-}
+policy := object.get(input, "default_policy", {})
-limits_compliant := false if {
- policy != null
- object.get(policy, "ActionWhenThresholdReached", missing_sentinel) == missing_sentinel
-}
-
-limits_compliant := false if {
- policy != null
- some k
- required_policy_fields[k]
- object.get(policy, k, missing_sentinel) != missing_sentinel
- policy[k] != required_policy_fields[k]
+required_policy_fields := {
+ "RecipientLimitExternalPerHour": 500,
+ "RecipientLimitInternalPerHour": 1000,
+ "RecipientLimitPerDay": 1000,
+ "ActionWhenThresholdReached": "BlockUser",
+ "NotifyOutboundSpamRecipients": {"monitored@example.com"}
}
-limits_compliant := false if {
- policy != null
- object.get(policy, "ActionWhenThresholdReached", missing_sentinel) != missing_sentinel
- not policy.ActionWhenThresholdReached in valid_actions
+# Function to validate individual policy settings
+validate_policy_setting(setting_name, setting_value) if {
+ required_policy_fields[setting_name] == setting_value
}
-policy := value if {
- input != null
- value := object.get(input, "default_policy", null)
-} else = null
-has_policy := policy != null
-
-safe_get(obj, key) = value if {
- obj != null
- value := object.get(obj, key, null)
-} else = null
-
-policy_detail_fields := {
- "RecipientLimitExternalPerHour",
- "RecipientLimitInternalPerHour",
- "RecipientLimitPerDay",
- "ActionWhenThresholdReached",
+validate_notify_outbound_spam_recipients if {
+ count(policy.NotifyOutboundSpamRecipients) > 0
}
-add_if_not_null(obj, key, value) = out if {
- value != null
- out := object.union(obj, {key: value})
-} else = obj
-
-details := out if {
- base := {
- "required_policy_settings": required_policy_fields,
- "valid_actions": valid_actions,
- }
-
- v_external := safe_get(policy, "RecipientLimitExternalPerHour")
- d1 := add_if_not_null(base, "RecipientLimitExternalPerHour", v_external)
-
- v_internal := safe_get(policy, "RecipientLimitInternalPerHour")
- d2 := add_if_not_null(d1, "RecipientLimitInternalPerHour", v_internal)
-
- v_day := safe_get(policy, "RecipientLimitPerDay")
- d3 := add_if_not_null(d2, "RecipientLimitPerDay", v_day)
-
- v_action := safe_get(policy, "ActionWhenThresholdReached")
- out := add_if_not_null(d3, "ActionWhenThresholdReached", v_action)
+compliant if {
+ # Validate that all required policy fields match
+ count({
+ k |
+ required_policy_fields[k] == policy[k]
+ }) == count(required_policy_fields)
}
-compliant := true if {
- has_policy
- limits_compliant
-}
+compliant_message := "Outbound spam filter policy is correctly configured and meets required standards"
-compliant := false if {
- has_policy
- not limits_compliant
-}
+non_compliant_message := "Outbound spam filter policy settings are misconfigured or incomplete"
-compliant := false if {
- not has_policy
-}
+unknown_message := "Unable to determine outbound spam filter policy configuration"
-compliant_message := "Outbound spam filter policy is correctly configured for message limits and over-limit action"
-non_compliant_message := "Outbound spam filter policy settings for message limits or over-limit action are misconfigured"
generate_message(true) := compliant_message
generate_message(false) := non_compliant_message
+generate_message(null) := unknown_message
generate_affected_resources(true, _) := []
-generate_affected_resources(false, _) := ["Outbound Spam Filter Policy"]
+
+generate_affected_resources(false, data_input) := [
+ "Outbound Spam Filter Policy"
+]
+
+generate_affected_resources(null, _) := ["Outbound Spam Filter Policy configuration status unknown"]
result := {
- "compliant": compliant,
- "message": generate_message(compliant),
- "affected_resources": generate_affected_resources(compliant, input),
- "details": details
+ "compliant": compliant == true,
+ "message": generate_message(compliant),
+ "affected_resources": generate_affected_resources(compliant, input),
+ "details": {
+ "RecipientLimitExternalPerHour": policy.RecipientLimitExternalPerHour,
+ "RecipientLimitInternalPerHour": policy.RecipientLimitInternalPerHour,
+ "RecipientLimitPerDay": policy.RecipientLimitPerDay,
+ "ActionWhenThresholdReached": policy.ActionWhenThresholdReached,
+ "NotifyOutboundSpamRecipients": policy.NotifyOutboundSpamRecipients,
+ "required_policy_settings": required_policy_fields
+ }
}
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.1_SafeLinks_OfficeApplications_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.1_SafeLinks_OfficeApplications_Enabled.rego
index 930f55d0..1df9ea82 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.1_SafeLinks_OfficeApplications_Enabled.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.1_SafeLinks_OfficeApplications_Enabled.rego
@@ -20,104 +20,48 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_1
-import rego.v1
-
default result := {"compliant": false, "message": "Evaluation failed"}
-required_fields := {
- "EnableSafeLinksForEmail": true,
- "EnableSafeLinksForTeams": true,
- "EnableSafeLinksForOffice": true,
- "TrackClicks": true,
- "AllowClickThrough": false,
- "ScanUrls": true,
- "EnableForInternalSenders": true,
- "DeliverMessageAfterScan": true,
- "DisableUrlRewrite": false
-}
-
-missing_sentinel := "__missing__"
-
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) == missing_sentinel
-}
-
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) != missing_sentinel
- p[f] != required_fields[f]
-}
-
-non_compliant_fields(p) := fields if {
- fields := {f |
- some f
- required_fields[f] = _ # f is a key in required_fields (even if value is false)
- field_non_compliant(p, f)
- }
-}
-
-policy_compliant(p) := true if {
- count(non_compliant_fields(p)) == 0
-}
-
-policy_compliant(p) := false if {
- count(non_compliant_fields(p)) > 0
-}
+safe_links_policies := object.get(input, "safe_links_policies", [])
-generate_message(true, _) := "All Safe Links policies for Office applications are compliant"
-generate_message_no_policies := "No Safe Links policies found for Office applications"
+office_enabled_policies := [p |
+ p := safe_links_policies[_]
+ p.EnableSafeLinksForOffice == true
+]
-generate_message(false, non_compliant) := sprintf(
- "%d Safe Links policy(ies) for Office applications are not compliant",
- [count(non_compliant)]
-)
+non_compliant_policies := [p |
+ p := safe_links_policies[_]
+ p.EnableSafeLinksForOffice != true
+]
-generate_affected_resources(true, _) := []
+policy_name(p) := name if {
+ name := object.get(p, "Name", null)
+ name != null
+} else := identity if {
+ identity := object.get(p, "Identity", null)
+ identity != null
+} else := "Unknown policy"
-generate_affected_resources(false, non_compliant) := resources if {
- resources := [
- {
- "identity": object.get(p, "Identity", object.get(p, "Name", null)),
- "non_compliant_fields": [f | f := non_compliant_fields(p)[_]]
- } |
- p := non_compliant[_]
- ]
-}
+generate_message(true, _) := "Safe Links for Office applications is enabled in at least one policy."
+generate_message(false, false) := "No Safe Links policies were found to evaluate."
+generate_message(false, true) := "Safe Links for Office applications is not enabled in any policy."
-no_policies_result := {
- "compliant": false,
- "message": generate_message_no_policies,
- "affected_resources": [{"identity": "Safe Links policy", "non_compliant_fields": ["missing_policy"]}],
- "details": {
- "policies_checked": [],
- "required_fields": required_fields
- }
-}
-
-with_policies_result(policies, non_compliant) := {
- "compliant": count(non_compliant) == 0,
- "message": generate_message(count(non_compliant) == 0, non_compliant),
- "affected_resources": generate_affected_resources(count(non_compliant) == 0, non_compliant),
- "details": {
- "policies_checked": [object.get(p, "Identity", object.get(p, "Name", null)) | p := policies[_]],
- "required_fields": required_fields
- }
-}
+generate_affected_resources(true, _, _) := []
+generate_affected_resources(false, false, _) := ["SafeLinksPolicy"]
+generate_affected_resources(false, true, non_compliant) := [policy_name(p) | p := non_compliant[_]]
result := output if {
- policies := object.get(input, "safe_links_policies", [])
- count(policies) == 0
- output := no_policies_result
-}
-
-result := output if {
- policies := object.get(input, "safe_links_policies", [])
- count(policies) > 0
-
- non_compliant := [
- p |
- p := policies[_]
- not policy_compliant(p)
- ]
-
- output := with_policies_result(policies, non_compliant)
+ has_policies := count(safe_links_policies) > 0
+ compliant := count(office_enabled_policies) > 0
+
+ output := {
+ "compliant": compliant,
+ "message": generate_message(compliant, has_policies),
+ "affected_resources": generate_affected_resources(compliant, has_policies, non_compliant_policies),
+ "details": {
+ "total_policies": count(safe_links_policies),
+ "office_enabled_count": count(office_enabled_policies),
+ "office_enabled_policies": [policy_name(p) | p := office_enabled_policies[_]]
+ }
+ }
}
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Filter_Enabled.rego
similarity index 61%
rename from engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Enabled.rego
rename to engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Filter_Enabled.rego
index a21a1916..52a5918d 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Enabled.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.2_Common_AttachmentTypes_Filter_Enabled.rego
@@ -19,12 +19,17 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_2
-import rego.v1
-
default result := {"compliant": false, "message": "Evaluation failed"}
-file_filter_enabled := true if input.enable_file_filter == true
-file_filter_enabled := false if input.enable_file_filter != true
+enable_file_filter := object.get(
+ input,
+ "enable_file_filter",
+ object.get(object.get(input, "default_policy", {}), "EnableFileFilter", null)
+)
+
+file_filter_enabled := true if enable_file_filter == true
+file_filter_enabled := false if enable_file_filter != true
+file_filter_enabled := null if enable_file_filter == null
result := output if {
compliant := file_filter_enabled == true
@@ -32,9 +37,9 @@ result := output if {
output := {
"compliant": compliant,
"message": generate_message(file_filter_enabled),
- "affected_resources": generate_affected_resources(file_filter_enabled),
+ "affected_resources": generate_affected_resources(file_filter_enabled, input),
"details": {
- "enablefilefilter": file_filter_enabled
+ "enable_file_filter": file_filter_enabled
}
}
}
@@ -49,5 +54,11 @@ generate_message(file_filter_enabled) := msg if {
msg := "The 'Enable the common attachments filter' is Off."
}
-generate_affected_resources(true) := []
-generate_affected_resources(false) := ["Common attachments filter is disabled"]
+generate_message(file_filter_enabled) := msg if {
+ file_filter_enabled == null
+ msg := "Unable to determine the 'Enable the common attachments filter' status."
+}
+
+generate_affected_resources(true, _) := []
+generate_affected_resources(false, data_input) := ["Common attachments filter is disabled"]
+generate_affected_resources(null, _) := ["Common attachments filter status unknown"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego
index 9c67cc3a..ae34171a 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.3_Notifications_InternalUsers_sendingMalware_Enabled.rego
@@ -1,8 +1,10 @@
# METADATA
-# title: Ensure Notifications for Internal Users Sending Malware is Enabled
+# title: Ensure notifications for internal users sending malware is Enabled
# description: |
-# This control ensures that administrators are notified when internal users send malware.
-# It helps security teams respond promptly to malware incidents.
+# Exchange Online Protection is Microsoft's cloud-based filtering service
+# that protects organizations against spam, malware, and other email threats.
+# EOP is included in all Microsoft 365 organizations with Exchange Online mailboxes.
+#
# related_resources:
# - ref: https://www.cisecurity.org/benchmark/microsoft_365
# description: CIS Microsoft 365 Foundations Benchmark
@@ -11,74 +13,77 @@
# framework: cis
# benchmark: microsoft-365-foundations
# version: v6.0.0
-# severity: high
+# severity: medium
# service: Exchange
# requires_permissions:
# - Exchange.Manage
package cis.microsoft_365_foundations.v6_0_0.control_2_1_3
-import rego.v1
+default result = {"compliant": false, "message": "Evaluation failed"}
-default result := {"compliant": false, "message": "Evaluation failed"}
+policies := object.get(input, "malware_filter_policies", [])
-required_values := {
- "EnableInternalSenderAdminNotifications": true,
- "InternalSenderAdminAddress": "" # empty value considered compliant
+policy_list := policies
+policy_list := [p] if {
+ count(policies) == 0
+ p := object.get(input, "default_policy", null)
+ p != null
}
-missing_sentinel := "__missing__"
+has_policies := count(policy_list) > 0
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) == missing_sentinel
-}
+notifications_enabled := true if { has_policies } else := false if { true }
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) != missing_sentinel
- p[f] != required_values[f]
+policy_name(policy) := name if {
+ name := object.get(policy, "Identity", null)
+ name != null
+} else := name if {
+ name := object.get(policy, "Name", null)
+ name != null
+} else := "Unknown policy"
+
+violating_policies[policy_name(policy)] if {
+ some i
+ policy := policy_list[i]
+ object.get(policy, "EnableInternalSenderAdminNotifications", null) != true
}
-policy_compliant(p) if {
- invalid_fields := {f |
- some f
- required_values[f]
- field_non_compliant(p, f)
- }
- count(invalid_fields) == 0
+violating_policies[policy_name(policy)] if {
+ some i
+ policy := policy_list[i]
+ object.get(policy, "EnableInternalSenderAdminNotifications", null) == true
+ object.get(policy, "InternalSenderAdminAddress", "") == ""
}
-policy := input.default_policy
-policies := [policy] if policy != null
-policies := [] if policy == null
+notifications_configured := true if {
+ has_policies
+ count(violating_policies) == 0
+} else := false if { true }
-non_compliant_policies := [
- {"policy": p.Name, "failed_fields": [f |
- some f
- required_values[f]
- field_non_compliant(p, f)
- ]} |
- p := policies[_]
- not policy_compliant(p)
-]
+compliant := true if {
+ notifications_enabled
+ notifications_configured
+} else := false if { true }
-result := {
- "compliant": true,
- "message": sprintf("All %d notification policies are compliant", [count(policies)])
-} if {
- count(policies) > 0
- count(non_compliant_policies) == 0
+result = output if {
+ output := {
+ "compliant": compliant,
+ "message": generate_message(compliant, has_policies),
+ "affected_resources": generate_affected_resources(compliant, has_policies, violating_policies),
+ "details": {
+ "notifications_enabled": notifications_enabled,
+ "notifications_configured": notifications_configured,
+ "policies_count": count(policy_list),
+ "violations_count": count(violating_policies)
+ }
+ }
}
-result := {
- "compliant": false,
- "message": sprintf("Non-compliant policies detected: %v", [non_compliant_policies])
-} if {
- count(non_compliant_policies) > 0
-}
+generate_message(true, _) := "Internal sender admin notifications are enabled and configured correctly."
+generate_message(false, false) := "No malware filter policies were found to evaluate internal sender admin notifications."
+generate_message(false, true) := "Internal sender admin notifications are not properly configured."
-result := {
- "compliant": false,
- "message": "No notification policies found"
-} if {
- count(policies) == 0
-}
+generate_affected_resources(true, _, _) := []
+generate_affected_resources(false, false, _) := ["MalwareFilterPolicy"]
+generate_affected_resources(false, true, violations) := [v | v := violations[_]]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.4_Safe_AttachementsPolicy_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.4_Safe_AttachementsPolicy_Enabled.rego
index 48d5fc1c..773dba94 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.4_Safe_AttachementsPolicy_Enabled.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.4_Safe_AttachementsPolicy_Enabled.rego
@@ -19,49 +19,36 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_4
-import rego.v1
-
default result := {"compliant": false, "message": "Evaluation failed"}
-target_policy_candidate(p) if {
- p.Identity == "Built-In Protection Policy"
-}
+safe_attachment_policies := object.get(input, "safe_attachment_policies", [])
-target_policy_candidate(p) if {
- p.Name == "Built-In Protection Policy"
-}
+policy_name(p) := name if {
+ name := object.get(p, "Name", null)
+ name != null
+} else := identity if {
+ identity := object.get(p, "Identity", null)
+ identity != null
+} else := "Unknown policy"
-target_policies := [p |
- p := input.safe_attachment_policies[_]
- target_policy_candidate(p)
+built_in_policies := [p |
+ p := safe_attachment_policies[_]
+ policy_name(p) == "Built-In Protection Policy"
]
-target_policy := target_policies[0] if count(target_policies) > 0
-
-policy_identity := target_policy.Identity if target_policy.Identity
-policy_identity := target_policy.Name if target_policy.Name
+has_policy := count(built_in_policies) > 0
+policy := built_in_policies[0] if { has_policy }
-has_policy := target_policy != null
+policy_identity := object.get(policy, "Identity", object.get(policy, "Name", "Unknown policy"))
+policy_enable := object.get(policy, "Enable", null)
+policy_action := object.get(policy, "Action", null)
+policy_quarantine_tag := object.get(policy, "QuarantineTag", null)
-policy_matches if {
- target_policy.Enable == true
- target_policy.Action == "Block"
- target_policy.QuarantineTag == "AdminOnlyAccessPolicy"
- policy_identity == "Built-In Protection Policy"
-}
-
-compliant := true if {
+compliant if {
has_policy
- policy_matches
-}
-
-compliant := false if {
- has_policy
- not policy_matches
-}
-
-compliant := null if {
- not has_policy
+ policy_enable == true
+ policy_action == "Block"
+ policy_quarantine_tag == "AdminOnlyAccessPolicy"
}
result := {
@@ -70,53 +57,44 @@ result := {
"affected_resources": affected_resources,
"details": {
"identity": policy_identity,
- "enable": target_policy.Enable,
- "action": target_policy.Action,
- "quarantine_tag": target_policy.QuarantineTag
+ "enable": policy_enable,
+ "action": policy_action,
+ "quarantine_tag": policy_quarantine_tag
}
} if {
- has_policy
+ true
}
-message := "Safe Attachments Built-In Protection Policy is enabled, blocking threats, and correctly configured." if compliant == true
-message := "Unable to determine Safe Attachments policy configuration." if compliant == null
-message := "Safe Attachments Built-In Protection Policy is disabled." if {
- compliant == false
- target_policy.Enable == false
-}
-message := "Safe Attachments policy action is not set to 'Block'." if {
- compliant == false
- target_policy.Enable == true
- target_policy.Action != "Block"
-}
-message := "Safe Attachments policy does not use the 'AdminOnlyAccessPolicy' quarantine tag." if {
- compliant == false
- target_policy.Enable == true
- target_policy.Action == "Block"
- target_policy.QuarantineTag != "AdminOnlyAccessPolicy"
-}
+message := "Safe Attachments Built-In Protection Policy is enabled, blocking threats, and correctly configured." if {
+ compliant
+} else := "Safe Attachments Built-In Protection Policy is disabled." if {
+ has_policy
+ policy_enable == false
+} else := "Safe Attachments policy action is not set to 'Block'." if {
+ has_policy
+ policy_enable == true
+ policy_action != "Block"
+} else := "Safe Attachments policy does not use the 'AdminOnlyAccessPolicy' quarantine tag." if {
+ has_policy
+ policy_enable == true
+ policy_action == "Block"
+ policy_quarantine_tag != "AdminOnlyAccessPolicy"
+} else := "Safe Attachments Built-In Protection Policy was not found." if {
+ not has_policy
+} else := "Unable to determine Safe Attachments policy configuration."
-affected_resources := [] if compliant == true
-affected_resources := ["Safe Attachments policy status unknown"] if compliant == null
-affected_resources := [sprintf("Non-compliant Safe Attachments policy: %v", [policy_identity])] if {
- compliant == false
- policy_identity
+affected_resources := [] if {
+ compliant
}
-affected_resources := ["Non-compliant Safe Attachments policy: Built-In Protection Policy"] if {
- compliant == false
- not policy_identity
+
+affected_resources := [
+ sprintf("Non-compliant Safe Attachments policy: %v", [policy_identity])
+] if {
+ not compliant
+ has_policy
}
-result := {
- "compliant": false,
- "message": "Unable to determine Safe Attachments policy configuration.",
- "affected_resources": ["Safe Attachments policy status unknown"],
- "details": {
- "identity": null,
- "enable": null,
- "action": null,
- "quarantine_tag": null
- }
-} if {
+affected_resources := ["Safe Attachments Built-In Protection Policy not found"] if {
+ not compliant
not has_policy
}
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego
index ff8aae1d..85d401ef 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.5_Safe_Attachments_SharePoint_OneDrive_MSTeams_Enabled.rego
@@ -18,67 +18,43 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_5
-import rego.v1
-
default result := {"compliant": false, "message": "Evaluation failed"}
-required_values := {
- "EnableATPForSPOTeamsODB": true,
- "EnableSafeDocs": true,
- "AllowSafeDocsOpen": false
-}
+policies := [p |
+ p := object.get(input, "atp_policy", null)
+ p != null
+]
-missing_sentinel := "__missing__"
+non_compliant_policies = [policy.Name |
+ policy := policies[_]
+ policy.EnableATPForSPOTeamsODB == false
+ policy.EnableSafeDocs == false
+ policy.AllowSafeDocsOpen == true
+]
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) == missing_sentinel
-}
+compliant := count(non_compliant_policies) == 0
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) != missing_sentinel
- p[f] != required_values[f]
+message_text := "Safe Attachments for SharePoint, OneDrive, and Teams is configured securely" if {
+ compliant == true
}
-policy_compliant(p) if {
- invalid_fields := {f |
- some f
- required_values[f] = _
- field_non_compliant(p, f)
- }
- count(invalid_fields) == 0
+message_text := "Safe Attachments for SharePoint, OneDrive, or Teams is not configured securely" if {
+ compliant == false
}
-policy := input.atp_policy
-policies := [policy] if policy != null
-policies := [] if policy == null
-
-non_compliant_policies := [ {"policy": p.Name, "failed_fields": [f |
- some f
- required_values[f] = _
- field_non_compliant(p, f)
-]} |
- p := policies[_]
- not policy_compliant(p)
-]
-
-result := {
- "compliant": true,
- "message": sprintf("All %d Safe Attachments policies are compliant", [count(policies)])
-} if {
- count(policies) > 0
- count(non_compliant_policies) == 0
+affected_list := [] if {
+ compliant == true
}
-result := {
- "compliant": false,
- "message": sprintf("Non-compliant policies detected: %v", [non_compliant_policies])
-} if {
- count(non_compliant_policies) > 0
+affected_list := non_compliant_policies if {
+ compliant == false
}
result := {
- "compliant": false,
- "message": "No Safe Attachments policies found"
-} if {
- count(policies) == 0
+ "compliant": compliant,
+ "message": message_text,
+ "affected_resources": affected_list,
+ "details": {
+ "policies_evaluated": policies
+ }
}
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego
index 250af297..22e7f1da 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.6_Exchange_OnlineSpam_Policies_Notify_Administrators.rego
@@ -3,15 +3,16 @@
# description: |
# In Microsoft 365 organizations with mailboxes in Exchange Online or
# standalone Exchange Online Protection organizations without Exchange
-# Online mailboxes, email messages are automatically protected against spam by EOP.
+# Online mailboxes, email messages are automatically protected against spam (junk email) by EOP.
+#
# related_resources:
# - ref: https://www.cisecurity.org/benchmark/microsoft_365
-# description: CIS Microsoft 365 Foundations benchmark
+# description: CIS Microsoft 365 Foundations Benchmark
# custom:
# control_id: CIS-2.1.6
# framework: cis
# benchmark: microsoft-365-foundations
-# version: v6.0
+# version: v6.0.0
# severity: medium
# service: Exchange
# requires_permissions:
@@ -19,61 +20,88 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_6
-import rego.v1
+default result := {"compliant": false, "message": "Evaluation failed"}
-bcc_suspicious_outbound_mail := object.get(input, "bcc_suspicious_outbound_mail", false)
-notify_outbound_spam := object.get(input, "notify_outbound_spam", false)
-auto_forwarding_mode := object.get(input, "auto_forwarding_mode", null)
+bcc_suspicious_outbound_mail := object.get(
+ input,
+ "bcc_suspicious_outbound_mail",
+ object.get(object.get(input, "default_policy", {}), "BccSuspiciousOutboundMail", null)
+)
-missing_bcc if {
- not bcc_suspicious_outbound_mail
-}
+notify_outbound_spam := object.get(
+ input,
+ "notify_outbound_spam",
+ object.get(object.get(input, "default_policy", {}), "NotifyOutboundSpam", null)
+)
-missing_notify if {
- not notify_outbound_spam
-}
+bcc_additional_recipients := object.get(
+ object.get(input, "default_policy", {}),
+ "BccSuspiciousOutboundAdditionalRecipients",
+ null
+)
-missing_settings := array.concat(
- [s | s := "bcc_suspicious_outbound_mail"; missing_bcc],
- [s | s := "notify_outbound_spam"; missing_notify]
+notify_outbound_spam_recipients := object.get(
+ object.get(input, "default_policy", {}),
+ "NotifyOutboundSpamRecipients",
+ null
)
-outbound_spam_monitoring_enabled if {
- count(missing_settings) == 0
+bcc_recipients_count := count(bcc_additional_recipients) if {
+ bcc_additional_recipients != null
+} else := 0
+
+notify_recipients_count := count(notify_outbound_spam_recipients) if {
+ notify_outbound_spam_recipients != null
+} else := 0
+
+outbound_spam_monitoring_enabled := true if {
+ bcc_suspicious_outbound_mail == true
+ notify_outbound_spam == true
+ bcc_additional_recipients != null
+ notify_outbound_spam_recipients != null
+ bcc_recipients_count > 0
+ notify_recipients_count > 0
}
-scan_result = {
- "compliant": true,
- "message": "Outbound spam BCC and notification settings are enabled",
- "affected_resources": [],
- "details": {
- "bcc_suspicious_outbound_mail": bcc_suspicious_outbound_mail,
- "notify_outbound_spam": notify_outbound_spam,
- "auto_forwarding_mode": auto_forwarding_mode
- }
-} if {
- outbound_spam_monitoring_enabled
+outbound_spam_monitoring_enabled := false if {
+ bcc_suspicious_outbound_mail != true
+} else := false if {
+ notify_outbound_spam != true
+} else := false if {
+ bcc_additional_recipients != null
+ bcc_recipients_count == 0
+} else := false if {
+ notify_outbound_spam_recipients != null
+ notify_recipients_count == 0
}
-scan_result = {
- "compliant": false,
- "message": generate_message_missing,
- "affected_resources": ["HostedOutboundSpamFilterPolicy"],
- "details": {
- "bcc_suspicious_outbound_mail": bcc_suspicious_outbound_mail,
- "notify_outbound_spam": notify_outbound_spam,
- "auto_forwarding_mode": auto_forwarding_mode
- }
-} if {
- not outbound_spam_monitoring_enabled
+outbound_spam_monitoring_enabled := null if {
+ bcc_suspicious_outbound_mail == null
+ notify_outbound_spam == null
+ bcc_additional_recipients == null
+ notify_outbound_spam_recipients == null
}
-result := scan_result
+result := output if {
+ compliant := outbound_spam_monitoring_enabled == true
-generate_message_missing := msg if {
- count(missing_settings) > 0
- msg := sprintf(
- "Outbound spam settings disabled or misconfigured: %v",
- missing_settings
- )
+ output := {
+ "compliant": compliant,
+ "message": generate_message(outbound_spam_monitoring_enabled),
+ "affected_resources": generate_affected_resources(outbound_spam_monitoring_enabled),
+ "details": {
+ "bcc_suspicious_outbound_mail": bcc_suspicious_outbound_mail,
+ "bcc_additional_recipients": bcc_additional_recipients,
+ "notify_outbound_spam": notify_outbound_spam,
+ "notify_outbound_spam_recipients": notify_outbound_spam_recipients
+ }
+ }
}
+
+generate_message(true) := "Outbound spam Bcc and notification settings are enabled and recipients are configured"
+generate_message(false) := "Outbound spam Bcc or notification settings are disabled or missing recipients"
+generate_message(null) := "Unable to determine outbound spam Bcc or notification configuration"
+
+generate_affected_resources(true) := []
+generate_affected_resources(false) := ["HostedOutboundSpamFilterPolicy"]
+generate_affected_resources(null) := ["HostedOutboundSpamFilterPolicy status unknown"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.7_AntiPhishing_Policy_is_created.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.7_AntiPhishing_Policy_is_created.rego
index 69d4b4b1..535562d4 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.7_AntiPhishing_Policy_is_created.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.7_AntiPhishing_Policy_is_created.rego
@@ -23,96 +23,91 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_1_7
-import rego.v1
-
-required_values := {
- "Enabled": true,
- "PhishThresholdLevel": 3,
- "EnableTargetedUserProtection": true,
- "EnableOrganizationDomainsProtection": true,
- "EnableMailboxIntelligence": true,
- "EnableMailboxIntelligenceProtection": true,
- "EnableSpoofIntelligence": true,
- "TargetedUserProtectionAction": "Quarantine",
- "TargetedDomainProtectionAction": "Quarantine",
- "MailboxIntelligenceProtectionAction": "Quarantine",
- "EnableFirstContactSafetyTips": true,
- "EnableSimilarUsersSafetyTips": true,
- "EnableSimilarDomainsSafetyTips": true,
- "EnableUnusualCharactersSafetyTips": true,
- "HonorDmarcPolicy": true
-}
+default result := {"compliant": false, "message": "Evaluation failed"}
-missing_sentinel := "__missing__"
+policies := object.get(input, "anti_phish_policies", object.get(input, "antiPhishPolicies", []))
+rules := object.get(input, "anti_phish_rules", object.get(input, "antiPhishRules", []))
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) == missing_sentinel
+required_policy_fields := {
+ "Enabled": true,
+ "PhishThresholdLevel": 3,
+ "EnableTargetedUserProtection": true,
+ "EnableOrganizationDomainsProtection": true,
+ "EnableMailboxIntelligence": true,
+ "EnableMailboxIntelligenceProtection": true,
+ "EnableSpoofIntelligence": true,
+ "TargetedUserProtectionAction": "Quarantine",
+ "TargetedDomainProtectionAction": "Quarantine",
+ "MailboxIntelligenceProtectionAction": "Quarantine",
+ "EnableFirstContactSafetyTips": true,
+ "EnableSimilarUsersSafetyTips": true,
+ "EnableSimilarDomainsSafetyTips": true,
+ "EnableUnusualCharactersSafetyTips": true,
+ "HonorDmarcPolicy": true
}
-field_non_compliant(p, f) if {
- object.get(p, f, missing_sentinel) != missing_sentinel
- p[f] != required_values[f]
-}
+matching_policy[policy_obj] if {
+ policy_obj := policies[_]
-policy_compliant(p) if {
- invalid_fields := {f |
- some f
- required_values[f]
- field_non_compliant(p, f)
- }
- count(invalid_fields) == 0
+ # All required fields must match
+ count({k | required_policy_fields[k] == policy_obj[k]}) == count(required_policy_fields)
+
+ # Targeted users must be within limits
+ count(policy_obj.TargetedUsersToProtect) > 0
+ count(policy_obj.TargetedUsersToProtect) <= 350
}
-policies := object.get(input, "anti_phish_policies", [])
+matching_rule[rule_obj] if {
+ rule_obj := rules[_]
+ rule_obj.State == "Enabled"
-non_compliant_policies := [
- {"policy": p.Name, "failed_fields": [f |
- some f
- required_values[f]
- field_non_compliant(p, f)
- ]} |
- p := policies[_]
- not policy_compliant(p)
-]
+ matching_policy[policy_obj] # ✅ unique variable name
+ rule_obj.AntiPhishPolicy == policy_obj.Name
+}
-targeted_users := [user |
- p := policies[_]
- policy_compliant(p)
- users := object.get(p, "TargetedUsersToProtect", [])
- user := users[_]
-]
+targets_majority(rule_obj) if {
+ count(rule_obj.RecipientDomainIs) > 0
+ count(rule_obj.SentToMemberOf) > 0
+}
-global_compliant_policies := [p |
- p := policies[_]
- policy_compliant(p)
- count(object.get(p, "TargetedUsersToProtect", [])) == 0
-]
+# Compliant: matching policy + enabled rule + targets majority
+antiphish_configured if {
+ matching_rule[rule_obj]
+ targets_majority(rule_obj)
+}
-result := {
- "compliant": false,
- "message": "No anti-phishing policies found"
-} if {
- count(policies) == 0
+# Non-compliant: policies exist but none match requirements
+antiphish_configured if {
+ count(policies) > 0
+ count({p | matching_policy[p]}) == 0
}
-result := {
- "compliant": false,
- "message": sprintf(
- "Non-compliant policies detected: %v",
- [non_compliant_policies]
- )
-} if {
- count(policies) > 0
- count(non_compliant_policies) > 0
+antiphish_configured = null if {
+ count(policies) == 0
+ count(rules) == 0
}
+generate_message(true) := "Anti-Phish policy and rules are correctly configured and enabled"
+generate_message(false) := "Anti-Phish policy or rules are misconfigured or not enforced"
+generate_message(null) := "Unable to determine Anti-Phish policy or rule configuration"
+
+generate_affected_resources(true, _) := []
+
+generate_affected_resources(false, data_input) := [
+ pol.Name |
+ pol := data_input[_]
+]
+
+generate_affected_resources(null, _) := ["Anti-Phish configuration status unknown"]
+
result := {
- "compliant": true,
- "message": sprintf(
- "Found %d user(s) protected by compliant anti-phishing policy (including global/default)",
- [count(targeted_users) + count(global_compliant_policies)]
- )
-} if {
- count(policies) > 0
- count(non_compliant_policies) == 0
+ "compliant": antiphish_configured == true,
+ "message": generate_message(antiphish_configured),
+ "affected_resources": generate_affected_resources(antiphish_configured, policies),
+ "details": {
+ "anti_phish_policies_evaluated": count(policies),
+ "anti_phish_rules_evaluated": count(rules),
+ "targeted_user_limit": 350,
+ "required_policy_settings": required_policy_fields
+ }
}
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.9_DKIM_is_enabled.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.9_DKIM_is_enabled.rego
index a4da6a8c..97cf4c63 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.9_DKIM_is_enabled.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.1.9_DKIM_is_enabled.rego
@@ -1,9 +1,9 @@
# METADATA
# title: Ensure that DKIM is enabled for all Exchange Online Domains
# description: |
-# DKIM is one of the trio of Authentication methods (SPF, DKIM and DMARC) that help
-# prevent attackers from sending messages that look like they come from your domain.
-#
+# DKIM is one of the trio of authentication methods (SPF, DKIM, and DMARC) that help
+# prevent attackers from sending messages that look like they come from your domain.
+#
# related_resources:
# - ref: https://www.cisecurity.org/benchmark/microsoft_365
# description: CIS Microsoft 365 Foundations Benchmark
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego
index b83d0f57..cf7c4774 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/2.4.4_Zero_hour_AutoPurge_MSTeams_is_On.rego
@@ -21,13 +21,29 @@
package cis.microsoft_365_foundations.v6_0_0.control_2_4_4
-import rego.v1
+default result := {"compliant": false, "message": "Evaluation failed"}
-zap_enabled := object.get(input, "zap_enabled", null)
+required_fields := {
+ "ZeroHourAutoPurgeEnabled": true
+}
+
+zap_enabled := object.get(
+ input,
+ "zap_enabled",
+ object.get(object.get(input, "teams_protection_policy", {}), "ZapEnabled", null)
+)
+
+zero_hour_auto_purge_enabled := true if {
+ zap_enabled == true
+}
-zero_hour_auto_purge_enabled := true if zap_enabled == true
-zero_hour_auto_purge_enabled := false if zap_enabled == false
-zero_hour_auto_purge_enabled := null if zap_enabled == null
+zero_hour_auto_purge_enabled := false if {
+ zap_enabled != true
+}
+
+zero_hour_auto_purge_enabled := null if {
+ not zap_enabled
+}
result := output if {
compliant := zero_hour_auto_purge_enabled == true
@@ -37,14 +53,15 @@ result := output if {
"message": generate_message(zero_hour_auto_purge_enabled),
"affected_resources": generate_affected_resources(zero_hour_auto_purge_enabled),
"details": {
- "zap_enabled": zap_enabled
+ "ZeroHourAutoPurgeEnabled": zap_enabled
}
}
}
generate_message(true) := "Zero-hour auto purge is enabled for Microsoft Teams"
generate_message(false) := "Zero-hour auto purge is not enabled for Microsoft Teams"
-generate_message(null) := "Unable to determine Zero-hour auto purge status for Microsoft Teams"
+generate_message(null) := "Unable to determine if Zero-hour auto purge is enabled for Microsoft Teams"
+
generate_affected_resources(true) := []
generate_affected_resources(false) := ["TeamsProtectionPolicy"]
generate_affected_resources(null) := ["TeamsProtectionPolicy status unknown"]
diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json
index 6f611a82..e3d70a57 100644
--- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json
+++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json
@@ -154,7 +154,7 @@
"automation_status": "ready",
"data_collector_id": "entra.applications.apps_and_services_settings",
"policy_file": "1.3.4_user_owned_apps_restricted.rego",
- "requires_permissions": ["Policy.Read.All"],
+ "requires_permissions": ["OrgSettings-AppsAndServices.Read.All"],
"notes": null
},
{
@@ -169,7 +169,7 @@
"automation_status": "ready",
"data_collector_id": "entra.applications.forms_settings",
"policy_file": "1.3.5_forms_internal_phishing_protection.rego",
- "requires_permissions": ["Policy.Read.All"],
+ "requires_permissions": ["OrgSettings-Forms.Read.All"],
"notes": null
},
{
@@ -258,7 +258,7 @@
"benchmark_audit_type": "Automated",
"automation_status": "ready",
"data_collector_id": "exchange.protection.malware_filter_policy",
- "policy_file": "2.1.2_Common_AttachmentTypes_Enabled.rego",
+ "policy_file": "2.1.2_Common_AttachmentTypes_Filter_Enabled.rego",
"requires_permissions": ["Exchange.Manage"],
"notes": null
},
diff --git a/engine/powershell/Dockerfile b/engine/powershell/Dockerfile
index a7e06373..140ba52d 100644
--- a/engine/powershell/Dockerfile
+++ b/engine/powershell/Dockerfile
@@ -1,7 +1,8 @@
-FROM mcr.microsoft.com/powershell:7.5-mariner-2.0
+ARG TARGETPLATFORM
+FROM --platform=$TARGETPLATFORM mcr.microsoft.com/powershell:7.5-mariner-2.0
-# Install Python, curl (for healthcheck), and tar (for uv installer)
-RUN tdnf install -y python3 curl tar && tdnf clean all
+# Install Python, curl (for healthcheck), tar (for uv installer), and awk (uv install script uses it)
+RUN tdnf install -y python3 curl tar gawk && tdnf clean all
# Install uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
diff --git a/engine/worker/config.py b/engine/worker/config.py
index b82ded1c..4ebe4927 100644
--- a/engine/worker/config.py
+++ b/engine/worker/config.py
@@ -26,6 +26,10 @@ class WorkerSettings(BaseSettings):
# PowerShell service URL (optional - if set, uses HTTP instead of Docker)
POWERSHELL_SERVICE_URL: str | None = None
+ # Performance mode: PowerShell-based controls (Exchange/Compliance/Teams) are much slower
+ # than Graph-based controls. Default is True to preserve full scan coverage.
+ ENABLE_POWERSHELL_CONTROLS: bool = True
+
class Config:
env_file = ".env"
diff --git a/engine/worker/tasks.py b/engine/worker/tasks.py
index 421f0c2b..94811146 100644
--- a/engine/worker/tasks.py
+++ b/engine/worker/tasks.py
@@ -126,8 +126,28 @@ def run_scan(scan_id: int) -> dict:
# Check automation_status before dispatching
status = control.get("automation_status", "ready")
+ collector_id = control.get("data_collector_id") or ""
if status == "ready":
+ # Optional fast-scan mode: allow skipping slow PowerShell-based controls only when
+ # explicitly disabled via ENABLE_POWERSHELL_CONTROLS=false.
+ if (
+ settings.ENABLE_POWERSHELL_CONTROLS is False
+ and collector_id.startswith(("exchange.", "compliance.", "teams."))
+ and not collector_id.startswith("exchange.dns.")
+ ):
+ with get_db_session() as session:
+ update_scan_result(
+ session,
+ result_id=result["id"],
+ status="skipped",
+ message="Skipped (fast scan): PowerShell-based controls disabled (ENABLE_POWERSHELL_CONTROLS=false).",
+ )
+ increment_scan_skipped_count(session, scan_id)
+ session.commit()
+ skipped += 1
+ continue
+
# Verify collector exists before dispatching
if not control.get("data_collector_id"):
with get_db_session() as session:
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 64ee2005..29c0e5d2 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -12,9 +12,10 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
- "chart.js": "^4.5.0",
+ "apexcharts": "^5.3.6",
"lucide-react": "^0.542.0",
"react": "^19.1.1",
+ "react-apexcharts": "^1.9.0",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.1"
},
@@ -25,12 +26,10 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
- "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "version": "7.27.1",
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -39,9 +38,7 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
- "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+ "version": "7.28.5",
"dev": true,
"license": "MIT",
"engines": {
@@ -49,21 +46,19 @@
}
},
"node_modules/@babel/core": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
- "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
+ "version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/generator": "^7.28.6",
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-module-transforms": "^7.28.6",
- "@babel/helpers": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/template": "^7.28.6",
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -79,25 +74,13 @@
"url": "https://opencollective.com/babel"
}
},
- "node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/generator": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
- "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+ "version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -107,13 +90,11 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
- "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "version": "7.27.2",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.28.6",
+ "@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@@ -123,20 +104,8 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
- "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -144,29 +113,25 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
- "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
- "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "version": "7.28.3",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.6"
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -177,8 +142,6 @@
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
- "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -187,8 +150,6 @@
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -197,8 +158,6 @@
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -206,8 +165,6 @@
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
- "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -215,27 +172,23 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
- "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "version": "7.28.4",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
- "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.6"
+ "@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -246,8 +199,6 @@
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
- "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -262,8 +213,6 @@
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
- "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -277,42 +226,36 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
- "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
+ "version": "7.28.4",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
- "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "version": "7.27.2",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
- "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+ "version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/generator": "^7.28.6",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
"@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.6",
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
"debug": "^4.3.1"
},
"engines": {
@@ -320,9 +263,7 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
- "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "version": "7.28.5",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -403,8 +344,6 @@
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
- "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
@@ -776,9 +715,7 @@
}
},
"node_modules/@fontsource/league-spartan": {
- "version": "5.2.7",
- "resolved": "https://registry.npmjs.org/@fontsource/league-spartan/-/league-spartan-5.2.7.tgz",
- "integrity": "sha512-OwYjMcJOGAGB680+4+Z47c/G5Ky8f8mT9Md6e8DB8/6fi1RGd/Ct5pUe3KT/lhUeNGUT8m5R4PKww47qtU8NHw==",
+ "version": "5.2.8",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -786,8 +723,6 @@
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -797,8 +732,6 @@
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -808,38 +741,19 @@
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.11",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
- "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.30",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
- "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "version": "0.3.31",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -847,23 +761,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@kurkle/color": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
- "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
- "license": "MIT"
- },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.53",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
- "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz",
- "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
+ "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
"cpu": [
"arm"
],
@@ -875,9 +781,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz",
- "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
+ "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
"cpu": [
"arm64"
],
@@ -889,9 +795,7 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz",
- "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==",
+ "version": "4.53.5",
"cpu": [
"arm64"
],
@@ -903,9 +807,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz",
- "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
+ "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
"cpu": [
"x64"
],
@@ -917,9 +821,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz",
- "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
+ "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
"cpu": [
"arm64"
],
@@ -931,9 +835,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz",
- "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
+ "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
"cpu": [
"x64"
],
@@ -945,9 +849,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz",
- "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
+ "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
"cpu": [
"arm"
],
@@ -959,9 +863,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz",
- "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
+ "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
"cpu": [
"arm"
],
@@ -973,9 +877,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz",
- "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
+ "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
"cpu": [
"arm64"
],
@@ -987,9 +891,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz",
- "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
+ "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
"cpu": [
"arm64"
],
@@ -1001,23 +905,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz",
- "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz",
- "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
+ "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
"cpu": [
"loong64"
],
@@ -1029,23 +919,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz",
- "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz",
- "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
+ "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
"cpu": [
"ppc64"
],
@@ -1057,9 +933,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz",
- "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
+ "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
"cpu": [
"riscv64"
],
@@ -1071,9 +947,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz",
- "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
+ "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
"cpu": [
"riscv64"
],
@@ -1085,9 +961,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz",
- "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
+ "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
"cpu": [
"s390x"
],
@@ -1099,9 +975,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz",
- "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
+ "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
"cpu": [
"x64"
],
@@ -1113,9 +989,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz",
- "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
+ "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
"cpu": [
"x64"
],
@@ -1126,24 +1002,10 @@
"linux"
]
},
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz",
- "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz",
- "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
+ "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
"cpu": [
"arm64"
],
@@ -1155,9 +1017,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz",
- "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
+ "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
"cpu": [
"arm64"
],
@@ -1169,9 +1031,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz",
- "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
+ "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
"cpu": [
"ia32"
],
@@ -1183,9 +1045,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz",
- "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
+ "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
"cpu": [
"x64"
],
@@ -1197,9 +1059,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz",
- "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==",
+ "version": "4.53.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
+ "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
"cpu": [
"x64"
],
@@ -1210,10 +1072,64 @@
"win32"
]
},
+ "node_modules/@svgdotjs/svg.draggable.js": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
+ "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@svgdotjs/svg.js": "^3.2.4"
+ }
+ },
+ "node_modules/@svgdotjs/svg.filter.js": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
+ "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@svgdotjs/svg.js": "^3.2.4"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/@svgdotjs/svg.js": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz",
+ "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Fuzzyma"
+ }
+ },
+ "node_modules/@svgdotjs/svg.resize.js": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
+ "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18"
+ },
+ "peerDependencies": {
+ "@svgdotjs/svg.js": "^3.2.4",
+ "@svgdotjs/svg.select.js": "^4.0.1"
+ }
+ },
+ "node_modules/@svgdotjs/svg.select.js": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
+ "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18"
+ },
+ "peerDependencies": {
+ "@svgdotjs/svg.js": "^3.2.4"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "10.4.1",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
- "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.10.4",
@@ -1229,19 +1145,8 @@
"node": ">=18"
}
},
- "node_modules/@testing-library/dom/node_modules/aria-query": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
- "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
- "license": "Apache-2.0",
- "dependencies": {
- "dequal": "^2.0.3"
- }
- },
"node_modules/@testing-library/react": {
- "version": "16.3.0",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
- "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
+ "version": "16.3.1",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
@@ -1267,8 +1172,6 @@
},
"node_modules/@testing-library/user-event": {
"version": "13.5.0",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
- "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
@@ -1283,14 +1186,10 @@
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
- "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1303,8 +1202,6 @@
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1313,8 +1210,6 @@
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1324,8 +1219,6 @@
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
- "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1334,27 +1227,11 @@
},
"node_modules/@types/estree": {
"version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
- "node_modules/@types/node": {
- "version": "24.3.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
- "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "undici-types": "~7.10.0"
- }
- },
"node_modules/@vitejs/plugin-react": {
"version": "5.1.2",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
- "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1372,44 +1249,60 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
- "node_modules/@vitejs/plugin-react/node_modules/react-refresh": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
- "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
- "dev": true,
+ "node_modules/@yr/monotone-cubic-spline": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
+ "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
"license": "MIT",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
}
},
- "node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "dev": true,
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
"license": "MIT",
- "optional": true,
- "peer": true,
- "bin": {
- "acorn": "bin/acorn"
- },
"engines": {
- "node": ">=0.4.0"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
+ "node_modules/apexcharts": {
+ "version": "5.3.6",
+ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.6.tgz",
+ "integrity": "sha512-sVEPw+J0Gp0IHQabKu8cfdsxlfME0e36Wid7RIaPclGM2OUt+O7O4+6mfAmTUYhy5bDk8cNHzEhPfVtLCIXEJA==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@svgdotjs/svg.draggable.js": "^3.0.4",
+ "@svgdotjs/svg.filter.js": "^3.0.8",
+ "@svgdotjs/svg.js": "^3.2.4",
+ "@svgdotjs/svg.resize.js": "^2.0.2",
+ "@svgdotjs/svg.select.js": "^4.0.1",
+ "@yr/monotone-cubic-spline": "^1.0.3"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.9",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/browserslist": {
- "version": "4.25.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
- "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
+ "version": "4.28.1",
"dev": true,
"funding": [
{
@@ -1427,10 +1320,11 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001737",
- "electron-to-chromium": "^1.5.211",
- "node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.3"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -1439,19 +1333,8 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/caniuse-lite": {
- "version": "1.0.30001739",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
- "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
+ "version": "1.0.30001760",
"dev": true,
"funding": [
{
@@ -1469,29 +1352,24 @@
],
"license": "CC-BY-4.0"
},
- "node_modules/chart.js": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
- "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
- "license": "MIT",
- "dependencies": {
- "@kurkle/color": "^0.3.0"
- },
- "engines": {
- "pnpm": ">=8"
- }
- },
"node_modules/convert-source-map": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "version": "4.4.3",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1508,8 +1386,6 @@
},
"node_modules/dequal": {
"version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -1517,21 +1393,15 @@
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
- "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.213",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz",
- "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==",
+ "version": "1.5.267",
"dev": true,
"license": "ISC"
},
"node_modules/esbuild": {
"version": "0.27.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
- "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1572,20 +1442,31 @@
},
"node_modules/escalade": {
"version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
- "hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1597,36 +1478,18 @@
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
- "node_modules/jiti": {
- "version": "1.21.7",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
- "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "bin": {
- "jiti": "bin/jiti.js"
- }
- },
"node_modules/js-tokens": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1638,8 +1501,6 @@
},
"node_modules/json5": {
"version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1649,10 +1510,20 @@
"node": ">=6"
}
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -1661,8 +1532,6 @@
},
"node_modules/lucide-react": {
"version": "0.542.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz",
- "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1670,8 +1539,6 @@
},
"node_modules/lz-string": {
"version": "1.5.0",
- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
@@ -1679,15 +1546,11 @@
},
"node_modules/ms": {
"version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
@@ -1704,22 +1567,36 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "version": "2.0.27",
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
@@ -1747,8 +1624,6 @@
},
"node_modules/pretty-format": {
"version": "27.5.1",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
- "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
@@ -1759,49 +1634,67 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
- "node_modules/pretty-format/node_modules/ansi-styles": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
}
},
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/react": {
- "version": "19.1.1",
- "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
- "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
+ "version": "19.2.3",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/react-apexcharts": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.9.0.tgz",
+ "integrity": "sha512-DDBzQFuKdwyCEZnji1yIcjlnV8hRr4VDabS5Y3iuem/WcTq6n4VbjWPzbPm3aOwW4I+rf/gA3zWqhws4z9CwLw==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "apexcharts": ">=4.0.0",
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/react-dom": {
- "version": "19.1.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
- "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
+ "version": "19.2.3",
"license": "MIT",
"dependencies": {
- "scheduler": "^0.26.0"
+ "scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.1.1"
+ "react": "^19.2.3"
}
},
"node_modules/react-is": {
"version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT"
},
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react-router": {
- "version": "7.9.1",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz",
- "integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==",
+ "version": "7.11.0",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -1821,12 +1714,10 @@
}
},
"node_modules/react-router-dom": {
- "version": "7.9.1",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.1.tgz",
- "integrity": "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==",
+ "version": "7.11.0",
"license": "MIT",
"dependencies": {
- "react-router": "7.9.1"
+ "react-router": "7.11.0"
},
"engines": {
"node": ">=20.0.0"
@@ -1836,103 +1727,77 @@
"react-dom": ">=18"
}
},
- "node_modules/react-router/node_modules/cookie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "node_modules/rollup": {
+ "version": "4.53.5",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.5",
+ "@rollup/rollup-android-arm64": "4.53.5",
+ "@rollup/rollup-darwin-arm64": "4.53.5",
+ "@rollup/rollup-darwin-x64": "4.53.5",
+ "@rollup/rollup-freebsd-arm64": "4.53.5",
+ "@rollup/rollup-freebsd-x64": "4.53.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.5",
+ "@rollup/rollup-linux-arm64-musl": "4.53.5",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.5",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.5",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.5",
+ "@rollup/rollup-linux-x64-gnu": "4.53.5",
+ "@rollup/rollup-linux-x64-musl": "4.53.5",
+ "@rollup/rollup-openharmony-arm64": "4.53.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.5",
+ "@rollup/rollup-win32-x64-gnu": "4.53.5",
+ "@rollup/rollup-win32-x64-msvc": "4.53.5",
+ "fsevents": "~2.3.2"
}
},
"node_modules/scheduler": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
- "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "version": "0.27.0",
"license": "MIT"
},
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/set-cookie-parser": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "version": "2.7.2",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "node_modules/source-map-support/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/tailwindcss": {
"version": "4.1.18",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
- "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"dev": true,
"license": "MIT"
},
- "node_modules/terser": {
- "version": "5.44.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
- "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.15.0",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "bin": {
- "terser": "bin/terser"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/terser/node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/tinyglobby": {
"version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1946,50 +1811,8 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/undici-types": {
- "version": "7.10.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
- "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/update-browserslist-db": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
- "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "version": "1.2.3",
"dev": true,
"funding": [
{
@@ -2018,9 +1841,7 @@
}
},
"node_modules/vite": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
- "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "version": "7.3.0",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2092,106 +1913,10 @@
}
}
},
- "node_modules/vite/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/vite/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/vite/node_modules/rollup": {
- "version": "4.55.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz",
- "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.8"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.55.2",
- "@rollup/rollup-android-arm64": "4.55.2",
- "@rollup/rollup-darwin-arm64": "4.55.2",
- "@rollup/rollup-darwin-x64": "4.55.2",
- "@rollup/rollup-freebsd-arm64": "4.55.2",
- "@rollup/rollup-freebsd-x64": "4.55.2",
- "@rollup/rollup-linux-arm-gnueabihf": "4.55.2",
- "@rollup/rollup-linux-arm-musleabihf": "4.55.2",
- "@rollup/rollup-linux-arm64-gnu": "4.55.2",
- "@rollup/rollup-linux-arm64-musl": "4.55.2",
- "@rollup/rollup-linux-loong64-gnu": "4.55.2",
- "@rollup/rollup-linux-loong64-musl": "4.55.2",
- "@rollup/rollup-linux-ppc64-gnu": "4.55.2",
- "@rollup/rollup-linux-ppc64-musl": "4.55.2",
- "@rollup/rollup-linux-riscv64-gnu": "4.55.2",
- "@rollup/rollup-linux-riscv64-musl": "4.55.2",
- "@rollup/rollup-linux-s390x-gnu": "4.55.2",
- "@rollup/rollup-linux-x64-gnu": "4.55.2",
- "@rollup/rollup-linux-x64-musl": "4.55.2",
- "@rollup/rollup-openbsd-x64": "4.55.2",
- "@rollup/rollup-openharmony-arm64": "4.55.2",
- "@rollup/rollup-win32-arm64-msvc": "4.55.2",
- "@rollup/rollup-win32-ia32-msvc": "4.55.2",
- "@rollup/rollup-win32-x64-gnu": "4.55.2",
- "@rollup/rollup-win32-x64-msvc": "4.55.2",
- "fsevents": "~2.3.2"
- }
- },
"node_modules/yallist": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
- },
- "node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 5311fae3..84d63361 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,9 +7,10 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
- "chart.js": "^4.5.0",
+ "apexcharts": "^5.3.6",
"lucide-react": "^0.542.0",
"react": "^19.1.1",
+ "react-apexcharts": "^1.9.0",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.1"
},
@@ -32,7 +33,7 @@
},
"devDependencies": {
"@vitejs/plugin-react": "^5.1.2",
- "vite": "^7.3.0",
- "tailwindcss": "^4.1.18"
+ "tailwindcss": "^4.1.18",
+ "vite": "^7.3.0"
}
}
diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
index 2f100907..8db31aa1 100644
--- a/frontend/src/api/client.js
+++ b/frontend/src/api/client.js
@@ -156,6 +156,18 @@ export async function getContactHistory(token, id) {
return fetchWithAuth(`/v1/contact/submissions/${id}/history`, token);
}
+// Settings endpoints
+export async function getSettings(token) {
+ return fetchWithAuth('/v1/settings', token);
+}
+
+export async function updateSettings(token, data) {
+ return fetchWithAuth('/v1/settings', token, {
+ method: 'PATCH',
+ body: JSON.stringify(data),
+ });
+}
+
// Platform endpoints
export async function getPlatforms(token) {
return fetchWithAuth('/v1/platforms', token);
@@ -224,6 +236,23 @@ export async function createScan(token, data) {
});
}
+export async function deleteScan(token, id) {
+ const response = await fetch(`${API_BASE_URL}/v1/scans/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ detail: response.statusText }));
+ throw new Error(error.detail || 'Failed to delete scan');
+ }
+
+ // DELETE returns 204 No Content, so don't try to parse JSON
+ return;
+}
+
// Evidence scanner endpoints
export async function getEvidenceStrategies() {
// Frontend -> Backend
diff --git a/frontend/src/components/ComplianceChart.jsx b/frontend/src/components/ComplianceChart.jsx
index 4cf73807..6fe1482a 100644
--- a/frontend/src/components/ComplianceChart.jsx
+++ b/frontend/src/components/ComplianceChart.jsx
@@ -1,174 +1,251 @@
-//This component establishes a chart to display the number of scan items which returned as passed, requiring attention, or failed
-//Last updated 18 September 2025
-//Recomended next changes:
-// -add a switch case for more chart types besides just pie and doughnot. Some of the chart types have slightly different syntax to each other so a little more work is needed on this.
-// - the legend is functional but the text doesn't look amazing. Played around with font settings and layout but can't get it to look quite right. Worth reviewing!
-
-//Note: no css for this component as this is purely javascript (no JSX) and the libary in use (charts.js) has its own parameters for styling
-
-//To render this component:
-//import ComplianceChart from './components/ComplianceChart';
-//
You do not have permission to view this page.
+Review and manage incoming Contact Us requests.
+Loading submissions...
+ ) : submissions.length ? ( +{submission.subject}
+No submissions yet.
+ )} +{note.note}
++ {new Date(note.created_at).toLocaleString()} +
++ {entry.action === "note" && entry.new_value + ? `Added note: \"${entry.new_value}\"` + : entry.field_name + ? `${entry.field_name} changed to: ${entry.new_value || "—"}` + : "Submission updated"} +
++ {new Date(entry.created_at).toLocaleString()} +
+Choose a submission to see details, notes, and history.
+
-
+
{stat.label}
-{stat.value}
-{stat.subtitle}
-{summary.subtitle}
+Latest activity for your selected connection/benchmark
Critical security gaps
-No scans found for the current filters.
+Important improvements needed
+ ) : ( +| Status | +Started | +Results | +Open | +
|---|---|---|---|
| + + {String(s.status || 'pending').toUpperCase()} + + | +
+
+
+ {dt.date}
+ {dt.time}
+ |
+
+
+ {passed} pass
+ {failed} fail
+ {errors > 0 && {errors} err}
+
+ |
+
+ |
+
Ready for next scan
+Top failing controls from the latest scan
+ {scanDetailsError ? ( +{scanDetailsError}
+No scan selected.
+Loading control results…
+No failed/error controls in this scan.
+