Compare commits

..

5 Commits

Author SHA1 Message Date
pedrooot
8e8e1f96c1 chore(changelog): update with latest changes 2026-03-17 12:34:02 +01:00
pedrooot
8140e3b7dd fix: typo 2026-03-17 12:30:37 +01:00
pedrooot
cd22112952 chore(revision): add missing changes 2026-03-17 12:27:38 +01:00
Tejas Saubhage
d778a7589c Add RBI Cyber Security Framework compliance for GCP fixes #8783 2026-03-15 10:32:07 -04:00
Alejandro Bailo
22f79edec5 refactor(ui): replace HeroUI Snippet with CodeSnippet component (#10319) 2026-03-13 16:31:39 +01:00
20 changed files with 329 additions and 229 deletions

View File

@@ -0,0 +1,20 @@
import warnings
from dashboard.common_methods import get_section_containers_rbi
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_rbi(aux, "REQUIREMENTS_ID")

View File

@@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `entra_conditional_access_policy_device_code_flow_blocked` check for M365 provider [(#10218)](https://github.com/prowler-cloud/prowler/pull/10218)
- RBI compliance for the Azure provider [(#10339)](https://github.com/prowler-cloud/prowler/pull/10339)
### 🔄 Changed
@@ -16,10 +17,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update M365 Entra ID service metadata to new format [(#9682)](https://github.com/prowler-cloud/prowler/pull/9682)
- Update ResourceType and Categories for Azure Entra ID service metadata [(#10334)](https://github.com/prowler-cloud/prowler/pull/10334)
### 🐞 Fixed
- Duplicate Kubernetes RBAC findings when the same User or Group subject appeared in multiple ClusterRoleBindings [(#10242)](https://github.com/prowler-cloud/prowler/pull/10242)
### 🔐 Security
- Bump `multipart` to 1.3.1 to fix [GHSA-p2m9-wcp5-6qw3](https://github.com/defnull/multipart/security/advisories/GHSA-p2m9-wcp5-6qw3) [(#10331)](https://github.com/prowler-cloud/prowler/pull/10331)

View File

@@ -0,0 +1,178 @@
{
"Framework": "RBI-Cyber-Security-Framework",
"Name": "Reserve Bank of India (RBI) Cyber Security Framework",
"Version": "",
"Provider": "GCP",
"Description": "The Reserve Bank had prescribed a set of baseline cyber security controls for primary (Urban) cooperative banks (UCBs) in October 2018. On further examination, it has been decided to prescribe a comprehensive cyber security framework for the UCBs, as a graded approach, based on their digital depth and interconnectedness with the payment systems landscape, digital products offered by them and assessment of cyber security risk. The framework would mandate implementation of progressively stronger security measures based on the nature, variety and scale of digital product offerings of banks.",
"Requirements": [
{
"Id": "annex_i_1_1",
"Name": "Annex I (1.1)",
"Description": "UCBs should maintain an up-to-date business IT Asset Inventory Register containing the following fields, as a minimum: a) Details of the IT Asset (viz., hardware/software/network devices, key personnel, services, etc.), b. Details of systems where customer data are stored, c. Associated business applications, if any, d. Criticality of the IT asset (For example, High/Medium/Low).",
"Attributes": [
{
"ItemId": "annex_i_1_1",
"Service": "gcp"
}
],
"Checks": [
"iam_cloud_asset_inventory_enabled",
"iam_organization_essential_contacts_configured"
]
},
{
"Id": "annex_i_1_3",
"Name": "Annex I (1.3)",
"Description": "Appropriately manage and provide protection within and outside UCB/network, keeping in mind how the data/information is stored, transmitted, processed, accessed and put to use within/outside the UCB's network, and level of risk they are exposed to depending on the sensitivity of the data/information.",
"Attributes": [
{
"ItemId": "annex_i_1_3",
"Service": "gcp"
}
],
"Checks": [
"cloudstorage_bucket_public_access",
"cloudstorage_bucket_uniform_bucket_level_access",
"cloudstorage_bucket_logging_enabled",
"cloudstorage_bucket_versioning_enabled",
"cloudsql_instance_public_access",
"cloudsql_instance_public_ip",
"cloudsql_instance_ssl_connections",
"compute_instance_public_ip",
"compute_instance_encryption_with_csek_enabled",
"compute_image_not_publicly_shared",
"kms_key_rotation_enabled",
"kms_key_not_publicly_accessible",
"bigquery_dataset_public_access",
"bigquery_dataset_cmk_encryption"
]
},
{
"Id": "annex_i_5_1",
"Name": "Annex I (5.1)",
"Description": "The firewall configurations should be set to the highest security level and evaluation of critical device (such as firewall, network switches, security devices, etc.) configurations should be done periodically.",
"Attributes": [
{
"ItemId": "annex_i_5_1",
"Service": "compute"
}
],
"Checks": [
"compute_firewall_rdp_access_from_the_internet_allowed",
"compute_firewall_ssh_access_from_the_internet_allowed",
"compute_network_not_legacy",
"compute_network_default_in_use",
"compute_subnet_flow_logs_enabled",
"compute_network_dns_logging_enabled",
"dns_dnssec_disabled"
]
},
{
"Id": "annex_i_6",
"Name": "Annex I (6)",
"Description": "Put in place systems and processes to identify, track, manage and monitor the status of patches to servers, operating system and application software running at the systems used by the UCB officials (end-users). Implement and update antivirus protection for all servers and applicable end points preferably through a centralised system.",
"Attributes": [
{
"ItemId": "annex_i_6",
"Service": "gcp"
}
],
"Checks": [
"compute_instance_shielded_vm_enabled",
"compute_project_os_login_enabled",
"artifacts_container_analysis_enabled",
"gcr_container_scanning_enabled"
]
},
{
"Id": "annex_i_7_1",
"Name": "Annex I (7.1)",
"Description": "Disallow administrative rights on end-user workstations/PCs/laptops and provide access rights on a 'need to know' and 'need to do' basis.",
"Attributes": [
{
"ItemId": "annex_i_7_1",
"Service": "iam"
}
],
"Checks": [
"iam_sa_no_administrative_privileges",
"iam_no_service_roles_at_project_level",
"iam_role_sa_enforce_separation_of_duties",
"iam_role_kms_enforce_separation_of_duties",
"iam_sa_no_user_managed_keys",
"compute_instance_default_service_account_in_use",
"compute_instance_default_service_account_in_use_with_full_api_access"
]
},
{
"Id": "annex_i_7_2",
"Name": "Annex I (7.2)",
"Description": "Passwords should be set as complex and lengthy and users should not use same passwords for all the applications/systems/devices.",
"Attributes": [
{
"ItemId": "annex_i_7_2",
"Service": "iam"
}
],
"Checks": [
"compute_project_os_login_2fa_enabled",
"iam_account_access_approval_enabled"
]
},
{
"Id": "annex_i_7_3",
"Name": "Annex I (7.3)",
"Description": "Remote Desktop Protocol (RDP) which allows others to access the computer remotely over a network or over the internet should be always disabled and should be enabled only with the approval of the authorised officer of the UCB. Logs for such remote access shall be enabled and monitored for suspicious activities.",
"Attributes": [
{
"ItemId": "annex_i_7_3",
"Service": "compute"
}
],
"Checks": [
"compute_firewall_rdp_access_from_the_internet_allowed",
"compute_firewall_ssh_access_from_the_internet_allowed",
"compute_project_os_login_enabled"
]
},
{
"Id": "annex_i_7_4",
"Name": "Annex I (7.4)",
"Description": "Implement appropriate (e.g. centralised) systems and controls to allow, manage, log and monitor privileged/super user/administrative access to critical systems (servers/databases, applications, network devices etc.)",
"Attributes": [
{
"ItemId": "annex_i_7_4",
"Service": "logging"
}
],
"Checks": [
"iam_audit_logs_enabled",
"logging_sink_created",
"cloudstorage_audit_logs_enabled",
"compute_loadbalancer_logging_enabled",
"compute_subnet_flow_logs_enabled",
"logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
"logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
"logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled"
]
},
{
"Id": "annex_i_12",
"Name": "Annex I (12)",
"Description": "Take periodic back up of the important data and store this data 'off line' (i.e., transferring important files to a storage device that can be detached from a computer/system after copying all the files).",
"Attributes": [
{
"ItemId": "annex_i_12",
"Service": "gcp"
}
],
"Checks": [
"cloudsql_instance_automated_backups",
"cloudstorage_bucket_versioning_enabled",
"cloudstorage_bucket_soft_delete_enabled",
"cloudstorage_bucket_lifecycle_management_enabled",
"compute_snapshot_not_outdated"
]
}
]
}

View File

@@ -11,29 +11,24 @@ resources = ["certificatesigningrequests/approval"]
class rbac_minimize_csr_approval_access(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
# Collect unique subjects and the ClusterRole names bound to them
subjects_bound_roles = {}
for crb in rbac_client.cluster_role_bindings.values():
for subject in crb.subjects:
# CIS benchmarks scope these checks to human identities only
if subject.kind in ["User", "Group"]:
key = (subject.kind, subject.name, subject.namespace)
if key not in subjects_bound_roles:
subjects_bound_roles[key] = (subject, set())
subjects_bound_roles[key][1].add(crb.roleRef.name)
for _, (subject, role_names) in subjects_bound_roles.items():
report = Check_Report_Kubernetes(metadata=self.metadata(), resource=subject)
report.resource_name = f"{subject.kind}:{subject.name}"
report.resource_id = f"{subject.kind}/{subject.name}"
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to update the CSR approval sub-resource."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name in role_names:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to update the CSR approval sub-resource."
break
findings.append(report)
report = Check_Report_Kubernetes(
metadata=self.metadata(), resource=subject
)
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to update the CSR approval sub-resource."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name == crb.roleRef.name:
if is_rule_allowing_permissions(
cr.rules,
resources,
verbs,
):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to update the CSR approval sub-resource."
break
findings.append(report)
return findings

View File

@@ -11,29 +11,20 @@ resources = ["nodes/proxy"]
class rbac_minimize_node_proxy_subresource_access(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
# Collect unique subjects and the ClusterRole names bound to them
subjects_bound_roles = {}
for crb in rbac_client.cluster_role_bindings.values():
for subject in crb.subjects:
# CIS benchmarks scope these checks to human identities only
if subject.kind in ["User", "Group"]:
key = (subject.kind, subject.name, subject.namespace)
if key not in subjects_bound_roles:
subjects_bound_roles[key] = (subject, set())
subjects_bound_roles[key][1].add(crb.roleRef.name)
for _, (subject, role_names) in subjects_bound_roles.items():
report = Check_Report_Kubernetes(metadata=self.metadata(), resource=subject)
report.resource_name = f"{subject.kind}:{subject.name}"
report.resource_id = f"{subject.kind}/{subject.name}"
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to the node proxy sub-resource."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name in role_names:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to the node proxy sub-resource."
break
findings.append(report)
report = Check_Report_Kubernetes(
metadata=self.metadata(), resource=subject
)
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to the node proxy sub-resource."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name == crb.roleRef.name:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to the node proxy sub-resource."
break
findings.append(report)
return findings

View File

@@ -11,29 +11,21 @@ resources = ["persistentvolumes"]
class rbac_minimize_pv_creation_access(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
# Collect unique subjects and the ClusterRole names bound to them
subjects_bound_roles = {}
# Check each ClusterRoleBinding for access to create PersistentVolumes
for crb in rbac_client.cluster_role_bindings.values():
for subject in crb.subjects:
# CIS benchmarks scope these checks to human identities only
if subject.kind in ["User", "Group"]:
key = (subject.kind, subject.name, subject.namespace)
if key not in subjects_bound_roles:
subjects_bound_roles[key] = (subject, set())
subjects_bound_roles[key][1].add(crb.roleRef.name)
for _, (subject, role_names) in subjects_bound_roles.items():
report = Check_Report_Kubernetes(metadata=self.metadata(), resource=subject)
report.resource_name = f"{subject.kind}:{subject.name}"
report.resource_id = f"{subject.kind}/{subject.name}"
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to create PersistentVolumes."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name in role_names:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to create PersistentVolumes."
break
findings.append(report)
report = Check_Report_Kubernetes(
metadata=self.metadata(), resource=subject
)
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to create PersistentVolumes."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name == crb.roleRef.name:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to create PersistentVolumes."
break
findings.append(report)
return findings

View File

@@ -11,29 +11,20 @@ resources = ["serviceaccounts/token"]
class rbac_minimize_service_account_token_creation(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
# Collect unique subjects and the ClusterRole names bound to them
subjects_bound_roles = {}
for crb in rbac_client.cluster_role_bindings.values():
for subject in crb.subjects:
# CIS benchmarks scope these checks to human identities only
if subject.kind in ["User", "Group"]:
key = (subject.kind, subject.name, subject.namespace)
if key not in subjects_bound_roles:
subjects_bound_roles[key] = (subject, set())
subjects_bound_roles[key][1].add(crb.roleRef.name)
for _, (subject, role_names) in subjects_bound_roles.items():
report = Check_Report_Kubernetes(metadata=self.metadata(), resource=subject)
report.resource_name = f"{subject.kind}:{subject.name}"
report.resource_id = f"{subject.kind}/{subject.name}"
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to create service account tokens."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name in role_names:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to create service account tokens."
break
findings.append(report)
report = Check_Report_Kubernetes(
metadata=self.metadata(), resource=subject
)
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to create service account tokens."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name == crb.roleRef.name:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to create service account tokens."
break
findings.append(report)
return findings

View File

@@ -14,29 +14,24 @@ verbs = ["create", "update", "delete"]
class rbac_minimize_webhook_config_access(Check):
def execute(self) -> Check_Report_Kubernetes:
findings = []
# Collect unique subjects and the ClusterRole names bound to them
subjects_bound_roles = {}
for crb in rbac_client.cluster_role_bindings.values():
for subject in crb.subjects:
# CIS benchmarks scope these checks to human identities only
if subject.kind in ["User", "Group"]:
key = (subject.kind, subject.name, subject.namespace)
if key not in subjects_bound_roles:
subjects_bound_roles[key] = (subject, set())
subjects_bound_roles[key][1].add(crb.roleRef.name)
for _, (subject, role_names) in subjects_bound_roles.items():
report = Check_Report_Kubernetes(metadata=self.metadata(), resource=subject)
report.resource_name = f"{subject.kind}:{subject.name}"
report.resource_id = f"{subject.kind}/{subject.name}"
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to create, update, or delete webhook configurations."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name in role_names:
if is_rule_allowing_permissions(cr.rules, resources, verbs):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to create, update, or delete webhook configurations."
break
findings.append(report)
report = Check_Report_Kubernetes(
metadata=self.metadata(), resource=subject
)
report.status = "PASS"
report.status_extended = f"User or group '{subject.name}' does not have access to create, update, or delete webhook configurations."
for cr in rbac_client.cluster_roles.values():
if cr.metadata.name == crb.roleRef.name:
if is_rule_allowing_permissions(
cr.rules,
resources,
verbs,
):
report.status = "FAIL"
report.status_extended = f"User or group '{subject.name}' has access to create, update, or delete webhook configurations."
break
findings.append(report)
return findings

View File

@@ -14,9 +14,9 @@ import { createSamlConfig, updateSamlConfig } from "@/actions/integrations";
import { AddIcon } from "@/components/icons";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { useToast } from "@/components/ui";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { CustomServerInput } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { SnippetChip } from "@/components/ui/entities";
import { FormButtons } from "@/components/ui/form";
import { apiBaseUrl } from "@/lib";
@@ -258,7 +258,11 @@ export const SamlConfigForm = ({
: `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`;
return (
<form ref={formRef} action={formAction} className="flex flex-col gap-2">
<form
ref={formRef}
action={formAction}
className="flex min-w-0 flex-col gap-2"
>
<div className="py-1 text-xs">
Need help configuring SAML SSO?{" "}
<CustomLink
@@ -304,9 +308,9 @@ export const SamlConfigForm = ({
<span className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
ACS URL:
</span>
<SnippetChip
<CodeSnippet
value={acsUrl}
ariaLabel="Copy ACS URL to clipboard"
ariaLabel="Copy ACS URL"
className="h-10 w-full"
/>
</div>
@@ -315,9 +319,9 @@ export const SamlConfigForm = ({
<span className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Audience:
</span>
<SnippetChip
<CodeSnippet
value="urn:prowler.com:sp"
ariaLabel="Copy Audience to clipboard"
ariaLabel="Copy Audience"
className="h-10 w-full"
/>
</div>

View File

@@ -1,11 +1,11 @@
"use client";
import { Snippet } from "@heroui/snippet";
import Link from "next/link";
import { AddIcon } from "../icons";
import { Button, Card, CardContent, CardHeader } from "../shadcn";
import { Separator } from "../shadcn/separator/separator";
import { CodeSnippet } from "../ui/code-snippet/code-snippet";
import { DateWithTime } from "../ui/entities";
interface InvitationDetailsProps {
@@ -89,20 +89,7 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
Share this link with the user:
</h3>
<div className="flex w-full flex-col items-start justify-between overflow-hidden">
<Snippet
classNames={{
base: "w-full max-w-full",
content: "min-w-0 overflow-hidden",
pre: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
}}
hideSymbol
variant="bordered"
className="bg-bg-neutral-secondary max-w-full overflow-hidden py-1"
>
<p className="min-w-0 truncate text-sm">{invitationLink}</p>
</Snippet>
</div>
<CodeSnippet value={invitationLink} className="max-w-full" />
</CardContent>
</Card>
<div className="flex w-full items-center justify-end">

View File

@@ -1,8 +1,7 @@
"use client";
import { Snippet } from "@heroui/snippet";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { DateWithTime, EntityInfo, InfoField } from "@/components/ui/entities";
import { StatusBadge } from "@/components/ui/table/status-badge";
import { ProviderProps, ProviderType, ScanProps, TaskDetails } from "@/types";
@@ -81,18 +80,17 @@ export const ScanDetail = ({
</div>
<InfoField label="Scan ID" variant="simple">
<Snippet hideSymbol>{scanDetails.id}</Snippet>
<CodeSnippet value={scanDetails.id} />
</InfoField>
{scan.state === "failed" && taskDetails?.attributes.result && (
<>
{taskDetails.attributes.result.exc_message && (
<InfoField label="Error Message" variant="simple">
<Snippet hideSymbol>
<span className="text-xs whitespace-pre-line">
{taskDetails.attributes.result.exc_message.join("\n")}
</span>
</Snippet>
<CodeSnippet
value={taskDetails.attributes.result.exc_message.join("\n")}
multiline
/>
</InfoField>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">

View File

@@ -21,6 +21,10 @@ interface CodeSnippetProps {
icon?: ReactNode;
/** Function to format the displayed text (value is still copied as-is) */
formatter?: (value: string) => string;
/** Enable multiline display (disables truncation, enables word wrap) */
multiline?: boolean;
/** Custom aria-label for the copy button */
ariaLabel?: string;
}
export const CodeSnippet = ({
@@ -30,6 +34,8 @@ export const CodeSnippet = ({
hideCopyButton = false,
icon,
formatter,
multiline = false,
ariaLabel = "Copy to clipboard",
}: CodeSnippetProps) => {
const [copied, setCopied] = useState(false);
@@ -46,7 +52,7 @@ export const CodeSnippet = ({
type="button"
onClick={handleCopy}
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer transition-colors"
aria-label="Copy to clipboard"
aria-label={ariaLabel}
>
{copied ? (
<Check className="h-3.5 w-3.5" />
@@ -66,7 +72,7 @@ export const CodeSnippet = ({
"hover:bg-bg-neutral-tertiary text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer rounded-md p-1 transition-colors",
className,
)}
aria-label="Copy to clipboard"
aria-label={ariaLabel}
>
{copied ? (
<Check className="h-3.5 w-3.5" />
@@ -80,19 +86,26 @@ export const CodeSnippet = ({
return (
<div
className={cn(
"bg-bg-neutral-tertiary text-text-neutral-primary border-border-neutral-tertiary flex h-6 w-fit min-w-0 items-center gap-1.5 rounded-full border-2 px-2 py-0.5 text-xs",
"bg-bg-neutral-tertiary text-text-neutral-primary border-border-neutral-tertiary flex w-fit min-w-0 items-center gap-1.5 border-2 px-2 py-0.5 text-xs",
multiline ? "h-auto rounded-lg" : "h-6 rounded-full",
className,
)}
>
{icon && (
<span className="text-text-neutral-secondary shrink-0">{icon}</span>
)}
<Tooltip>
<TooltipTrigger asChild>
<code className="min-w-0 flex-1 truncate">{displayValue}</code>
</TooltipTrigger>
<TooltipContent side="top">{value}</TooltipContent>
</Tooltip>
{multiline ? (
<span className="min-w-0 flex-1 break-all whitespace-pre-wrap">
{displayValue}
</span>
) : (
<Tooltip>
<TooltipTrigger asChild>
<span className="min-w-0 flex-1 truncate">{displayValue}</span>
</TooltipTrigger>
<TooltipContent side="top">{value}</TooltipContent>
</Tooltip>
)}
{!hideCopyButton && <CopyButton />}
</div>
);

View File

@@ -3,4 +3,3 @@ export * from "./entity-info";
export * from "./get-provider-logo";
export * from "./info-field";
export * from "./scan-status";
export * from "./snippet-chip";

View File

@@ -1,51 +0,0 @@
import { Snippet } from "@heroui/snippet";
import { cn } from "@heroui/theme";
import { Tooltip } from "@heroui/tooltip";
import React from "react";
import { CopyIcon, DoneIcon } from "@/components/icons";
interface SnippetChipProps {
value: string;
ariaLabel?: string;
icon?: React.ReactNode;
hideCopyButton?: boolean;
formatter?: (value: string) => string;
className?: string;
}
export const SnippetChip = ({
value,
hideCopyButton = false,
ariaLabel = `Copy ${value} to clipboard`,
icon,
formatter,
className,
...props
}: SnippetChipProps) => {
return (
<Snippet
className={cn("h-6", className)}
classNames={{
content: "min-w-0 overflow-hidden",
pre: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
base: "border-border-neutral-tertiary bg-bg-neutral-tertiary rounded-lg border py-1",
}}
size="sm"
hideSymbol
copyIcon={<CopyIcon size={16} />}
checkIcon={<DoneIcon size={16} />}
hideCopyButton={hideCopyButton}
codeString={value}
{...props}
>
<div className="flex min-w-0 items-center gap-2" aria-label={ariaLabel}>
{icon}
<Tooltip content={value} placement="top" size="sm">
<span className="min-w-0 flex-1 truncate text-xs">
{formatter ? formatter(value) : value}
</span>
</Tooltip>
</div>
</Snippet>
);
};

View File

@@ -191,7 +191,7 @@ export function DataTablePagination({
{/* Page info and navigation */}
<div className="flex items-center gap-3">
<span className="text-text-neutral-secondary text-xs font-medium">
<span className="text-text-neutral-secondary hidden text-xs font-medium sm:inline">
Page {currentPage} of {totalPages}
</span>
<div className="flex items-center gap-3">

View File

@@ -1,10 +1,9 @@
"use client";
import { Snippet } from "@heroui/snippet";
import { Button } from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { Alert, AlertDescription } from "@/components/ui/alert/Alert";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
interface ApiKeySuccessModalProps {
isOpen: boolean;
@@ -36,18 +35,12 @@ export const ApiKeySuccessModal = ({
<p className="text-text-neutral-primary text-sm font-medium">
Your API Key
</p>
<Snippet
hideSymbol
classNames={{
pre: "font-mono text-sm break-all whitespace-pre-wrap p-2 text-text-neutral-primary",
}}
tooltipProps={{
content: "Copy API key",
color: "default",
}}
>
{apiKey}
</Snippet>
<CodeSnippet
value={apiKey}
multiline
ariaLabel="Copy API key"
className="w-full px-3 py-2 text-sm"
/>
</div>
</div>

View File

@@ -32,8 +32,8 @@ export const MembershipItem = ({
setIsOpen={setIsEditOpen}
/>
</Modal>
<Card variant="inner" className="min-w-[320px] p-2">
<div className="flex w-full items-center gap-4">
<Card variant="inner" className="p-2">
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<Chip size="sm" variant="flat" color="secondary">
{membership.attributes.role}
</Chip>

View File

@@ -1,6 +1,5 @@
"use client";
import { Snippet } from "@heroui/snippet";
import { Trash2Icon } from "lucide-react";
import { revokeApiKey } from "@/actions/api-keys/api-keys";
@@ -10,6 +9,7 @@ import {
AlertDescription,
AlertTitle,
} from "@/components/ui/alert/Alert";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { ModalButtons } from "@/components/ui/custom/custom-modal-buttons";
import { FALLBACK_VALUES } from "./api-keys/constants";
@@ -67,16 +67,12 @@ export const RevokeApiKeyModal = ({
<div className="flex flex-col gap-2">
<p>Are you sure you want to revoke this API key?</p>
<Snippet
hideSymbol
hideCopyButton={true}
classNames={{
pre: "font-mono text-sm break-all whitespace-pre-wrap",
}}
>
<p>{apiKey?.attributes.name || FALLBACK_VALUES.UNNAMED_KEY}</p>
<p className="mt-1 text-xs">Prefix: {apiKey?.attributes.prefix}</p>
</Snippet>
<CodeSnippet
value={`${apiKey?.attributes.name || FALLBACK_VALUES.UNNAMED_KEY}\nPrefix: ${apiKey?.attributes.prefix}`}
hideCopyButton
multiline
className="w-full px-3 py-2 text-sm"
/>
</div>
{error && (

View File

@@ -68,6 +68,7 @@ export const RoleItem = ({
variant="ghost"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
className="px-0"
>
{isExpanded ? "Hide details" : "Show details"}
</Button>

View File

@@ -4,13 +4,14 @@ import { Divider } from "@heroui/divider";
import { ProwlerShort } from "@/components/icons";
import { Card, CardContent } from "@/components/shadcn";
import { DateWithTime, InfoField, SnippetChip } from "@/components/ui/entities";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { DateWithTime, InfoField } from "@/components/ui/entities";
import { UserDataWithRoles } from "@/types/users";
const TenantIdCopy = ({ id }: { id: string }) => {
return (
<div className="flex max-w-full min-w-0 items-center gap-2 md:flex-col md:items-start md:justify-start">
<SnippetChip value={id} className="max-w-full" />
<CodeSnippet value={id} className="max-w-full" />
</div>
);
};