Add risk publish to document system
Replace the old snapshot-based system for risks with the publish document system, mirroring the prior vendor / processing activity / DPIA / TIA migration. Includes the GraphQL mutation, MCP tool, CLI command, n8n operation, frontend publish dialog, e2e tests, and a prosemirror register template covering name, description, category, treatment, owner, inherent and residual scoring, and notes. The risk register lives as a generated DocumentTypeRegister document on the organization, reused across publishes (the major version bumps on every republish). Approvers can be passed in to create a draft pending approval; otherwise the version is published immediately. The frontend Risks page exposes a Publish button and a Document link button when the document exists, and pre-fills the previous default approvers. Risks was the last remaining snapshot type, so this commit also removes the entire snapshot system: drop snapshotId from the Risk GraphQL type and RiskFilter; remove RiskSnapshotter, Risks.Snapshot, InsertRiskSnapshots, and the SnapshotID/SourceID fields on Risk; delete Snapshot, ControlSnapshot, SnapshotsType, SnapshotOrderField, Snapshottable, the SnapshotService, the Snapshot console resolvers and GraphQL schema, the Snapshot MCP types and operations (list/get/take/listControlSnapshots), the snapshot CLI (prb snapshot), the snapshot frontend pages, routes, banner, LinkedSnapshotsCard, SnapshotGraph, snapshot helpers, and the snapshot n8n resource and control link/unlink snapshot operations. The snapshot_id columns remain in the database but are now filtered out with snapshot_id IS NULL. Add Get/Upsert/Clear GeneratedDocumentID methods on Risk backed by a new risks_document_id column on generated_documents, matching the ProcessingActivity/Finding/Vendor pattern. The migration command migrate-risk-snapshots-to-documents uses raw SQL queries instead of the Go snapshot types, since those are gone. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -1,92 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import {
|
|
||||||
formatDate,
|
|
||||||
getSnapshotTypeLabel,
|
|
||||||
getSnapshotTypeUrlPath,
|
|
||||||
sprintf,
|
|
||||||
} from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { IconClock } from "@probo/ui";
|
|
||||||
import { graphql, useLazyLoadQuery } from "react-relay";
|
|
||||||
import { useLocation } from "react-router";
|
|
||||||
|
|
||||||
import type { SnapshotBannerQuery } from "#/__generated__/core/SnapshotBannerQuery.graphql";
|
|
||||||
|
|
||||||
const snapshotQuery = graphql`
|
|
||||||
query SnapshotBannerQuery($snapshotId: ID!) {
|
|
||||||
node(id: $snapshotId) {
|
|
||||||
... on Snapshot {
|
|
||||||
# eslint-disable-next-line relay/unused-fields
|
|
||||||
id
|
|
||||||
name
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const isSnapshotTypeValidForUrl = (type: string, pathname: string) => {
|
|
||||||
const urlPath = getSnapshotTypeUrlPath(type);
|
|
||||||
return pathname.includes(urlPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
snapshotId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SnapshotBanner({ snapshotId }: Props) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const data = useLazyLoadQuery<SnapshotBannerQuery>(snapshotQuery, {
|
|
||||||
snapshotId,
|
|
||||||
});
|
|
||||||
const snapshot = data.node;
|
|
||||||
|
|
||||||
if (!snapshot) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
snapshot.type
|
|
||||||
&& !isSnapshotTypeValidForUrl(snapshot.type, location.pathname)
|
|
||||||
) {
|
|
||||||
throw new Error("PAGE_NOT_FOUND");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-warning rounded-lg p-4 flex items-center gap-3">
|
|
||||||
<IconClock className="text-warning-600 flex-shrink-0" size={20} />
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span className="font-medium text-warning-800">
|
|
||||||
{__("Snapshot")}
|
|
||||||
{" "}
|
|
||||||
{snapshot.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-warning-700">
|
|
||||||
{sprintf(
|
|
||||||
__("You are viewing a %s snapshot from %s"),
|
|
||||||
getSnapshotTypeLabel(__, snapshot.type).toLocaleLowerCase(),
|
|
||||||
formatDate(snapshot.createdAt),
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import { getSnapshotTypeLabel, snapshotTypes } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Option } from "@probo/ui";
|
|
||||||
|
|
||||||
export function SnapshotTypeOptions() {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{snapshotTypes.map(type => (
|
|
||||||
<Option key={type} value={type}>
|
|
||||||
{getSnapshotTypeLabel(__, type)}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import {
|
|
||||||
formatDate,
|
|
||||||
getSnapshotTypeLabel,
|
|
||||||
getSnapshotTypeUrlPath,
|
|
||||||
sprintf,
|
|
||||||
} from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
IconChevronDown,
|
|
||||||
IconPlusLarge,
|
|
||||||
IconTrashCan,
|
|
||||||
Table,
|
|
||||||
Tbody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
Thead,
|
|
||||||
Tr,
|
|
||||||
TrButton,
|
|
||||||
} from "@probo/ui";
|
|
||||||
import { clsx } from "clsx";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { useFragment } from "react-relay";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
|
|
||||||
import type { LinkedSnapshotsCardFragment$key } from "#/__generated__/core/LinkedSnapshotsCardFragment.graphql";
|
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|
||||||
|
|
||||||
import { LinkedSnapshotsDialog } from "./LinkedSnapshotsDialog";
|
|
||||||
|
|
||||||
const linkedSnapshotFragment = graphql`
|
|
||||||
fragment LinkedSnapshotsCardFragment on Snapshot {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Mutation<Params> = (p: {
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
snapshotId: string;
|
|
||||||
} & Params;
|
|
||||||
connections: string[];
|
|
||||||
};
|
|
||||||
}) => void;
|
|
||||||
|
|
||||||
type Props<Params> = {
|
|
||||||
snapshots: (LinkedSnapshotsCardFragment$key & { id: string })[];
|
|
||||||
params: Params;
|
|
||||||
disabled?: boolean;
|
|
||||||
connectionId: string;
|
|
||||||
onAttach: Mutation<Params>;
|
|
||||||
onDetach: Mutation<Params>;
|
|
||||||
variant?: "card" | "table";
|
|
||||||
readOnly?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LinkedSnapshotsCard<Params>(props: Props<Params>) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const [limit, setLimit] = useState<number | null>(4);
|
|
||||||
const snapshots = useMemo(() => {
|
|
||||||
return limit ? props.snapshots.slice(0, limit) : props.snapshots;
|
|
||||||
}, [props.snapshots, limit]);
|
|
||||||
const showMoreButton = limit !== null && props.snapshots.length > limit;
|
|
||||||
const variant = props.variant ?? "table";
|
|
||||||
|
|
||||||
const onAttach = (snapshotId: string) => {
|
|
||||||
props.onAttach({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
snapshotId,
|
|
||||||
...props.params,
|
|
||||||
},
|
|
||||||
connections: [props.connectionId],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDetach = (snapshotId: string) => {
|
|
||||||
props.onDetach({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
snapshotId,
|
|
||||||
...props.params,
|
|
||||||
},
|
|
||||||
connections: [props.connectionId],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wrapper = variant === "card" ? Card : "div";
|
|
||||||
|
|
||||||
const colSpanTable = props.readOnly ? 4 : 5;
|
|
||||||
const colSpanCard = props.readOnly ? 3 : 4;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper padded className="space-y-[10px]">
|
|
||||||
{variant === "card" && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="text-lg font-semibold">{__("Snapshots")}</div>
|
|
||||||
{!props.readOnly && (
|
|
||||||
<LinkedSnapshotsDialog
|
|
||||||
disabled={props.disabled}
|
|
||||||
linkedSnapshots={props.snapshots}
|
|
||||||
onLink={onAttach}
|
|
||||||
onUnlink={onDetach}
|
|
||||||
>
|
|
||||||
<Button variant="tertiary" icon={IconPlusLarge}>
|
|
||||||
{__("Link snapshot")}
|
|
||||||
</Button>
|
|
||||||
</LinkedSnapshotsDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Table className={clsx(variant === "card" && "bg-invert")}>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th>{__("Name")}</Th>
|
|
||||||
<Th>{__("Type")}</Th>
|
|
||||||
{variant === "table" && <Th>{__("Description")}</Th>}
|
|
||||||
<Th>{__("Created")}</Th>
|
|
||||||
{!props.readOnly && <Th></Th>}
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{snapshots.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td
|
|
||||||
colSpan={variant === "table" ? colSpanTable : colSpanCard}
|
|
||||||
className="text-center text-txt-secondary"
|
|
||||||
>
|
|
||||||
{__("No snapshots linked")}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
{snapshots.map(snapshot => (
|
|
||||||
<SnapshotRow
|
|
||||||
key={snapshot.id}
|
|
||||||
snapshot={snapshot}
|
|
||||||
onClick={onDetach}
|
|
||||||
variant={variant}
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{variant === "table" && !props.readOnly && (
|
|
||||||
<LinkedSnapshotsDialog
|
|
||||||
disabled={props.disabled}
|
|
||||||
linkedSnapshots={props.snapshots}
|
|
||||||
onLink={onAttach}
|
|
||||||
onUnlink={onDetach}
|
|
||||||
>
|
|
||||||
<TrButton colspan={colSpanTable} icon={IconPlusLarge}>
|
|
||||||
{__("Link snapshot")}
|
|
||||||
</TrButton>
|
|
||||||
</LinkedSnapshotsDialog>
|
|
||||||
)}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
{showMoreButton && (
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={() => setLimit(null)}
|
|
||||||
className="mt-3 mx-auto"
|
|
||||||
icon={IconChevronDown}
|
|
||||||
>
|
|
||||||
{sprintf(__("Show %s more"), props.snapshots.length - limit)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SnapshotRow(props: {
|
|
||||||
snapshot: LinkedSnapshotsCardFragment$key & { id: string };
|
|
||||||
onClick: (snapshotId: string) => void;
|
|
||||||
variant: "card" | "table";
|
|
||||||
readOnly?: boolean;
|
|
||||||
}) {
|
|
||||||
const snapshot = useFragment(linkedSnapshotFragment, props.snapshot);
|
|
||||||
const organizationId = useOrganizationId();
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
const urlPath = getSnapshotTypeUrlPath(snapshot.type);
|
|
||||||
const snapshotUrl = `/organizations/${organizationId}/snapshots/${snapshot.id}${urlPath}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr to={snapshotUrl}>
|
|
||||||
<Td className="font-medium">{snapshot.name}</Td>
|
|
||||||
<Td>
|
|
||||||
<Badge variant="neutral">
|
|
||||||
{getSnapshotTypeLabel(__, snapshot.type)}
|
|
||||||
</Badge>
|
|
||||||
</Td>
|
|
||||||
{props.variant === "table" && (
|
|
||||||
<Td className="text-txt-secondary">
|
|
||||||
{snapshot.description || __("No description")}
|
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
<Td className="text-txt-tertiary">{formatDate(snapshot.createdAt)}</Td>
|
|
||||||
{!props.readOnly && (
|
|
||||||
<Td noLink width={50} className="text-end">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => props.onClick(snapshot.id)}
|
|
||||||
icon={IconTrashCan}
|
|
||||||
>
|
|
||||||
{__("Unlink")}
|
|
||||||
</Button>
|
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import { formatDate, getSnapshotTypeLabel } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
IconMagnifyingGlass,
|
|
||||||
IconPlusLarge,
|
|
||||||
IconTrashCan,
|
|
||||||
InfiniteScrollTrigger,
|
|
||||||
Input,
|
|
||||||
Spinner,
|
|
||||||
} from "@probo/ui";
|
|
||||||
import { type ReactNode, Suspense, useMemo, useState } from "react";
|
|
||||||
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
LinkedSnapshotsDialogFragment$data,
|
|
||||||
LinkedSnapshotsDialogFragment$key,
|
|
||||||
} from "#/__generated__/core/LinkedSnapshotsDialogFragment.graphql";
|
|
||||||
import type { LinkedSnapshotsDialogQuery } from "#/__generated__/core/LinkedSnapshotsDialogQuery.graphql";
|
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|
||||||
import type { NodeOf } from "#/types";
|
|
||||||
|
|
||||||
const snapshotsQuery = graphql`
|
|
||||||
query LinkedSnapshotsDialogQuery($organizationId: ID!) {
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
id
|
|
||||||
... on Organization {
|
|
||||||
...LinkedSnapshotsDialogFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const snapshotsFragment = graphql`
|
|
||||||
fragment LinkedSnapshotsDialogFragment on Organization
|
|
||||||
@refetchable(queryName: "LinkedSnapshotsDialogQuery_fragment")
|
|
||||||
@argumentDefinitions(
|
|
||||||
first: { type: "Int", defaultValue: 20 }
|
|
||||||
order: { type: "SnapshotOrder", defaultValue: null }
|
|
||||||
after: { type: "CursorKey", defaultValue: null }
|
|
||||||
before: { type: "CursorKey", defaultValue: null }
|
|
||||||
last: { type: "Int", defaultValue: null }
|
|
||||||
) {
|
|
||||||
snapshots(
|
|
||||||
first: $first
|
|
||||||
after: $after
|
|
||||||
last: $last
|
|
||||||
before: $before
|
|
||||||
orderBy: $order
|
|
||||||
) @connection(key: "LinkedSnapshotsDialogQuery_snapshots") {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
linkedSnapshots?: { id: string }[];
|
|
||||||
onLink: (snapshotId: string) => void;
|
|
||||||
onUnlink: (snapshotId: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LinkedSnapshotsDialog({ children, ...props }: Props) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog trigger={children} title={__("Link snapshots")}>
|
|
||||||
<DialogContent>
|
|
||||||
<Suspense fallback={<Spinner centered />}>
|
|
||||||
<LinkedSnapshotsDialogContent {...props} />
|
|
||||||
</Suspense>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogFooter exitLabel={__("Close")} />
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LinkedSnapshotsDialogContent(props: Omit<Props, "children">) {
|
|
||||||
const organizationId = useOrganizationId();
|
|
||||||
const query = useLazyLoadQuery<LinkedSnapshotsDialogQuery>(snapshotsQuery, {
|
|
||||||
organizationId,
|
|
||||||
});
|
|
||||||
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
|
|
||||||
LinkedSnapshotsDialogQuery,
|
|
||||||
LinkedSnapshotsDialogFragment$key
|
|
||||||
>(snapshotsFragment, query.organization);
|
|
||||||
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const snapshots = useMemo(
|
|
||||||
() => data.snapshots?.edges?.map(edge => edge.node) ?? [],
|
|
||||||
[data.snapshots],
|
|
||||||
);
|
|
||||||
const linkedIds = useMemo(() => {
|
|
||||||
return new Set(props.linkedSnapshots?.map(s => s.id) ?? []);
|
|
||||||
}, [props.linkedSnapshots]);
|
|
||||||
|
|
||||||
const filteredSnapshots = useMemo(() => {
|
|
||||||
return snapshots.filter(snapshot =>
|
|
||||||
snapshot.name.toLowerCase().includes(search.toLowerCase()),
|
|
||||||
);
|
|
||||||
}, [snapshots, search]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
|
|
||||||
<Input
|
|
||||||
icon={IconMagnifyingGlass}
|
|
||||||
placeholder={__("Search snapshots...")}
|
|
||||||
onValueChange={setSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-border-low">
|
|
||||||
{filteredSnapshots.map(snapshot => (
|
|
||||||
<SnapshotRow
|
|
||||||
key={snapshot.id}
|
|
||||||
snapshot={snapshot}
|
|
||||||
linkedSnapshots={linkedIds}
|
|
||||||
onLink={props.onLink}
|
|
||||||
onUnlink={props.onUnlink}
|
|
||||||
disabled={props.disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{hasNext && (
|
|
||||||
<InfiniteScrollTrigger
|
|
||||||
loading={isLoadingNext}
|
|
||||||
onView={() => loadNext(20)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Snapshot = NodeOf<LinkedSnapshotsDialogFragment$data["snapshots"]>;
|
|
||||||
|
|
||||||
type RowProps = {
|
|
||||||
snapshot: Snapshot;
|
|
||||||
linkedSnapshots: Set<string>;
|
|
||||||
disabled?: boolean;
|
|
||||||
onLink: (snapshotId: string) => void;
|
|
||||||
onUnlink: (snapshotId: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function SnapshotRow(props: RowProps) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
const isLinked = props.linkedSnapshots.has(props.snapshot.id);
|
|
||||||
const onClick = isLinked ? props.onUnlink : props.onLink;
|
|
||||||
const IconComponent = isLinked ? IconTrashCan : IconPlusLarge;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full"
|
|
||||||
onClick={() => onClick(props.snapshot.id)}
|
|
||||||
>
|
|
||||||
<div className="flex-1 flex items-center gap-4">
|
|
||||||
<div className="font-medium min-w-0 flex-shrink-0">
|
|
||||||
{props.snapshot.name}
|
|
||||||
</div>
|
|
||||||
<Badge variant="neutral" className="flex-shrink-0 ml-6">
|
|
||||||
{getSnapshotTypeLabel(__, props.snapshot.type)}
|
|
||||||
</Badge>
|
|
||||||
<div className="text-sm text-txt-secondary min-w-0 flex-1 text-left">
|
|
||||||
{props.snapshot.description || __("No description")}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-txt-tertiary flex-shrink-0">
|
|
||||||
{formatDate(props.snapshot.createdAt)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
disabled={props.disabled}
|
|
||||||
variant={isLinked ? "secondary" : "primary"}
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<IconComponent size={16} />
|
|
||||||
{" "}
|
|
||||||
{isLinked ? __("Unlink") : __("Link")}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -140,12 +140,6 @@ export const frameworkControlNodeQuery = graphql`
|
|||||||
canDeleteAuditMapping: permission(
|
canDeleteAuditMapping: permission(
|
||||||
action: "core:control:delete-audit-mapping"
|
action: "core:control:delete-audit-mapping"
|
||||||
)
|
)
|
||||||
canCreateSnapshotMapping: permission(
|
|
||||||
action: "core:control:create-snapshot-mapping"
|
|
||||||
)
|
|
||||||
canDeleteSnapshotMapping: permission(
|
|
||||||
action: "core:control:delete-snapshot-mapping"
|
|
||||||
)
|
|
||||||
canCreateObligationMapping: permission(
|
canCreateObligationMapping: permission(
|
||||||
action: "core:control:create-obligation-mapping"
|
action: "core:control:create-obligation-mapping"
|
||||||
)
|
)
|
||||||
@@ -192,16 +186,6 @@ export const frameworkControlNodeQuery = graphql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snapshots(first: 100)
|
|
||||||
@connection(key: "FrameworkGraphControl_snapshots") {
|
|
||||||
__id
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
...LinkedSnapshotsCardFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ export function useDeleteRiskMutation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const risksQuery = graphql`
|
export const risksQuery = graphql`
|
||||||
query RiskGraphListQuery($organizationId: ID!, $snapshotId: ID) {
|
query RiskGraphListQuery($organizationId: ID!) {
|
||||||
organization: node(id: $organizationId) {
|
organization: node(id: $organizationId) {
|
||||||
id
|
id
|
||||||
...RiskGraphFragment @arguments(snapshotId: $snapshotId)
|
...RiskGraphFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -70,22 +70,28 @@ const risksFragment = graphql`
|
|||||||
after: { type: "CursorKey", defaultValue: null }
|
after: { type: "CursorKey", defaultValue: null }
|
||||||
before: { type: "CursorKey", defaultValue: null }
|
before: { type: "CursorKey", defaultValue: null }
|
||||||
last: { type: "Int", defaultValue: null }
|
last: { type: "Int", defaultValue: null }
|
||||||
snapshotId: { type: "ID", defaultValue: null }
|
|
||||||
) {
|
) {
|
||||||
canCreateRisk: permission(action: "core:risk:create")
|
canCreateRisk: permission(action: "core:risk:create")
|
||||||
|
canPublishRisk: permission(action: "core:risk:publish")
|
||||||
|
risksDocument {
|
||||||
|
id
|
||||||
|
currentPublishedMajor
|
||||||
|
currentPublishedMinor
|
||||||
|
defaultApprovers {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
risks(
|
risks(
|
||||||
first: $first
|
first: $first
|
||||||
after: $after
|
after: $after
|
||||||
last: $last
|
last: $last
|
||||||
before: $before
|
before: $before
|
||||||
orderBy: $order
|
orderBy: $order
|
||||||
filter: { snapshotId: $snapshotId }
|
) @connection(key: "RisksListQuery_risks", filters: []) {
|
||||||
) @connection(key: "RisksListQuery_risks", filters: ["filter"]) {
|
|
||||||
__id
|
__id
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
snapshotId
|
|
||||||
name
|
name
|
||||||
category
|
category
|
||||||
treatment
|
treatment
|
||||||
@@ -130,7 +136,6 @@ export const riskNodeQuery = graphql`
|
|||||||
node(id: $riskId) {
|
node(id: $riskId) {
|
||||||
... on Risk {
|
... on Risk {
|
||||||
id
|
id
|
||||||
snapshotId
|
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
treatment
|
treatment
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { useConfirm } from "@probo/ui";
|
|
||||||
import { useMutation } from "react-relay";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
|
|
||||||
import { useMutationWithToasts } from "../useMutationWithToasts";
|
|
||||||
|
|
||||||
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
|
|
||||||
|
|
||||||
export const SnapshotsConnectionKey = "SnapshotsPage_snapshots";
|
|
||||||
|
|
||||||
export const snapshotsQuery = graphql`
|
|
||||||
query SnapshotGraphListQuery($organizationId: ID!) {
|
|
||||||
organization: node(id: $organizationId) {
|
|
||||||
... on Organization {
|
|
||||||
canCreateSnapshot: permission(action: "core:snapshot:create")
|
|
||||||
...SnapshotsPageFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const snapshotNodeQuery = graphql`
|
|
||||||
query SnapshotGraphNodeQuery($snapshotId: ID!) {
|
|
||||||
node(id: $snapshotId) {
|
|
||||||
... on Snapshot {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
organization {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const createSnapshotMutation = graphql`
|
|
||||||
mutation SnapshotGraphCreateMutation(
|
|
||||||
$input: CreateSnapshotInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge @prependEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const deleteSnapshotMutation = graphql`
|
|
||||||
mutation SnapshotGraphDeleteMutation(
|
|
||||||
$input: DeleteSnapshotInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
deleteSnapshot(input: $input) {
|
|
||||||
deletedSnapshotId @deleteEdge(connections: $connections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const useDeleteSnapshot = (
|
|
||||||
snapshot: { id: string; name: string },
|
|
||||||
connectionId: string,
|
|
||||||
) => {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const [mutate] = useMutationWithToasts(deleteSnapshotMutation, {
|
|
||||||
successMessage: __("Snapshot deleted successfully"),
|
|
||||||
errorMessage: __("Failed to delete snapshot"),
|
|
||||||
});
|
|
||||||
const confirm = useConfirm();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
confirm(
|
|
||||||
() =>
|
|
||||||
mutate({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
snapshotId: snapshot.id,
|
|
||||||
},
|
|
||||||
connections: [connectionId],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
message: sprintf(
|
|
||||||
__(
|
|
||||||
"This will permanently delete the snapshot %s. This action cannot be undone.",
|
|
||||||
),
|
|
||||||
snapshot.name,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCreateSnapshot = (connectionId: string) => {
|
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
|
||||||
const [mutate] = useMutation(createSnapshotMutation);
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
|
|
||||||
return (input: {
|
|
||||||
organizationId: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
}) => {
|
|
||||||
if (!input.organizationId) {
|
|
||||||
return alert(__("Failed to create snapshot: organization is required"));
|
|
||||||
}
|
|
||||||
if (!input.name) {
|
|
||||||
return alert(__("Failed to create snapshot: name is required"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return promisifyMutation(mutate)({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId: input.organizationId,
|
|
||||||
name: input.name,
|
|
||||||
description: input.description,
|
|
||||||
},
|
|
||||||
connections: [connectionId],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
IconBook,
|
IconBook,
|
||||||
IconBox,
|
IconBox,
|
||||||
IconCircleProgress,
|
IconCircleProgress,
|
||||||
IconClock,
|
|
||||||
IconFire3,
|
IconFire3,
|
||||||
IconGroup1,
|
IconGroup1,
|
||||||
IconInboxEmpty,
|
IconInboxEmpty,
|
||||||
@@ -62,7 +61,6 @@ const fragment = graphql`
|
|||||||
action: "core:processing-activity:list"
|
action: "core:processing-activity:list"
|
||||||
)
|
)
|
||||||
canListRightsRequests: permission(action: "core:rights-request:list")
|
canListRightsRequests: permission(action: "core:rights-request:list")
|
||||||
canListSnapshots: permission(action: "core:snapshot:list")
|
|
||||||
canGetTrustCenter: permission(action: "core:trust-center:get")
|
canGetTrustCenter: permission(action: "core:trust-center:get")
|
||||||
canListCookieBanners: permission(action: "core:cookie-banner:list")
|
canListCookieBanners: permission(action: "core:cookie-banner:list")
|
||||||
canUpdateOrganization: permission(action: "iam:organization:update")
|
canUpdateOrganization: permission(action: "iam:organization:update")
|
||||||
@@ -199,13 +197,6 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
|
|||||||
to={`${prefix}/rights-requests`}
|
to={`${prefix}/rights-requests`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{organization.canListSnapshots && (
|
|
||||||
<SidebarItem
|
|
||||||
label={__("Snapshots")}
|
|
||||||
icon={IconClock}
|
|
||||||
to={`${prefix}/snapshots`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{organization.canListAccessReviewCampaigns && (
|
{organization.canListAccessReviewCampaigns && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label={__("Access Reviews")}
|
label={__("Access Reviews")}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import { LinkedAuditsCard } from "#/components/audits/LinkedAuditsCard";
|
|||||||
import { LinkedDocumentsCard } from "#/components/documents/LinkedDocumentsCard";
|
import { LinkedDocumentsCard } from "#/components/documents/LinkedDocumentsCard";
|
||||||
import { LinkedMeasuresCard } from "#/components/measures/LinkedMeasuresCard";
|
import { LinkedMeasuresCard } from "#/components/measures/LinkedMeasuresCard";
|
||||||
import { LinkedObligationsCard } from "#/components/obligations/LinkedObligationsCard";
|
import { LinkedObligationsCard } from "#/components/obligations/LinkedObligationsCard";
|
||||||
import { LinkedSnapshotsCard } from "#/components/snapshots/LinkedSnapshotsCard";
|
|
||||||
import { frameworkControlNodeQuery } from "#/hooks/graph/FrameworkGraph";
|
import { frameworkControlNodeQuery } from "#/hooks/graph/FrameworkGraph";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
|
|
||||||
@@ -159,33 +158,6 @@ const detachObligationMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const attachSnapshotMutation = graphql`
|
|
||||||
mutation FrameworkControlPageAttachSnapshotMutation(
|
|
||||||
$input: CreateControlSnapshotMappingInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createControlSnapshotMapping(input: $input) {
|
|
||||||
snapshotEdge @prependEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
...LinkedSnapshotsCardFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const detachSnapshotMutation = graphql`
|
|
||||||
mutation FrameworkControlPageDetachSnapshotMutation(
|
|
||||||
$input: DeleteControlSnapshotMappingInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
deleteControlSnapshotMapping(input: $input) {
|
|
||||||
deletedSnapshotId @deleteEdge(connections: $connections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const deleteControlMutation = graphql`
|
const deleteControlMutation = graphql`
|
||||||
mutation FrameworkControlPageDeleteControlMutation(
|
mutation FrameworkControlPageDeleteControlMutation(
|
||||||
$input: DeleteControlInput!
|
$input: DeleteControlInput!
|
||||||
@@ -236,14 +208,6 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
|||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [attachAudit, isAttachingAudit] = useMutation(attachAuditMutation);
|
const [attachAudit, isAttachingAudit] = useMutation(attachAuditMutation);
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
const [detachSnapshot, isDetachingSnapshot] = useMutation(
|
|
||||||
detachSnapshotMutation,
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
|
||||||
const [attachSnapshot, isAttachingSnapshot] = useMutation(
|
|
||||||
attachSnapshotMutation,
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
|
||||||
const [deleteControl] = useMutation(deleteControlMutation);
|
const [deleteControl] = useMutation(deleteControlMutation);
|
||||||
|
|
||||||
// eslint-disable-next-line relay/generated-typescript-types
|
// eslint-disable-next-line relay/generated-typescript-types
|
||||||
@@ -267,10 +231,6 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
|||||||
const canUnlinkAudit = control.canDeleteAuditMapping;
|
const canUnlinkAudit = control.canDeleteAuditMapping;
|
||||||
const auditsReadOnly = !canLinkAudit && !canUnlinkAudit;
|
const auditsReadOnly = !canLinkAudit && !canUnlinkAudit;
|
||||||
|
|
||||||
const canLinkSnapshot = control.canCreateSnapshotMapping;
|
|
||||||
const canUnlinkSnapshot = control.canDeleteSnapshotMapping;
|
|
||||||
const snapshotsReadOnly = !canLinkSnapshot && !canUnlinkSnapshot;
|
|
||||||
|
|
||||||
const canLinkObligation = control.canCreateObligationMapping;
|
const canLinkObligation = control.canCreateObligationMapping;
|
||||||
const canUnlinkObligation = control.canDeleteObligationMapping;
|
const canUnlinkObligation = control.canDeleteObligationMapping;
|
||||||
const obligationsReadOnly = !canLinkObligation && !canUnlinkObligation;
|
const obligationsReadOnly = !canLinkObligation && !canUnlinkObligation;
|
||||||
@@ -482,27 +442,6 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
|||||||
readOnly={obligationsReadOnly}
|
readOnly={obligationsReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
|
||||||
<LinkedSnapshotsCard
|
|
||||||
variant="card"
|
|
||||||
snapshots={
|
|
||||||
control.snapshots?.edges.map(edge => edge.node)
|
|
||||||
?? []
|
|
||||||
}
|
|
||||||
params={{ controlId: control.id }}
|
|
||||||
connectionId={control.snapshots?.__id ?? ""}
|
|
||||||
onAttach={withErrorHandling(
|
|
||||||
attachSnapshot,
|
|
||||||
__("Failed to link snapshot"),
|
|
||||||
)}
|
|
||||||
onDetach={withErrorHandling(
|
|
||||||
detachSnapshot,
|
|
||||||
__("Failed to unlink snapshot"),
|
|
||||||
)}
|
|
||||||
disabled={isAttachingSnapshot || isDetachingSnapshot}
|
|
||||||
readOnly={snapshotsReadOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -114,10 +114,9 @@ export default function MeasureEvidencesTab() {
|
|||||||
const { measure } = useOutletContext<{
|
const { measure } = useOutletContext<{
|
||||||
measure: MeasureEvidencesTabFragment$key;
|
measure: MeasureEvidencesTabFragment$key;
|
||||||
}>();
|
}>();
|
||||||
const { measureId, evidenceId, snapshotId } = useParams<{
|
const { measureId, evidenceId } = useParams<{
|
||||||
measureId: string;
|
measureId: string;
|
||||||
evidenceId: string;
|
evidenceId: string;
|
||||||
snapshotId?: string;
|
|
||||||
}>();
|
}>();
|
||||||
if (!measureId) {
|
if (!measureId) {
|
||||||
throw new Error("Missing :measureId param in route");
|
throw new Error("Missing :measureId param in route");
|
||||||
@@ -132,7 +131,6 @@ export default function MeasureEvidencesTab() {
|
|||||||
const evidence = evidences.find(e => e.id === evidenceId);
|
const evidence = evidences.find(e => e.id === evidenceId);
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
|
|
||||||
usePageTitle(pagination.data.name + " - " + __("Evidences"));
|
usePageTitle(pagination.data.name + " - " + __("Evidences"));
|
||||||
|
|
||||||
@@ -156,11 +154,9 @@ export default function MeasureEvidencesTab() {
|
|||||||
measureId={measureId}
|
measureId={measureId}
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
hideActions={isSnapshotMode}
|
|
||||||
snapshotId={snapshotId}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isSnapshotMode && pagination.data.canUploadEvidence && (
|
{pagination.data.canUploadEvidence && (
|
||||||
<TrButton
|
<TrButton
|
||||||
colspan={5}
|
colspan={5}
|
||||||
onClick={() => dialogRef.current?.open()}
|
onClick={() => dialogRef.current?.open()}
|
||||||
@@ -175,16 +171,13 @@ export default function MeasureEvidencesTab() {
|
|||||||
<EvidencePreviewDialog
|
<EvidencePreviewDialog
|
||||||
key={evidence.id}
|
key={evidence.id}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
const baseUrl = isSnapshotMode
|
void navigate(`/organizations/${organizationId}/measures/${measureId}/evidences`);
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/measures/${measureId}/evidences`
|
|
||||||
: `/organizations/${organizationId}/measures/${measureId}/evidences`;
|
|
||||||
void navigate(baseUrl);
|
|
||||||
}}
|
}}
|
||||||
evidenceId={evidence.id}
|
evidenceId={evidence.id}
|
||||||
filename={evidence.file?.fileName || ""}
|
filename={evidence.file?.fileName || ""}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isSnapshotMode && pagination.data.canUploadEvidence && (
|
{pagination.data.canUploadEvidence && (
|
||||||
<CreateEvidenceDialog
|
<CreateEvidenceDialog
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
measureId={measureId}
|
measureId={measureId}
|
||||||
@@ -200,8 +193,6 @@ function EvidenceRow(props: {
|
|||||||
measureId: string;
|
measureId: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
hideActions?: boolean;
|
|
||||||
snapshotId?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const evidence = useFragment(evidenceFragment, props.evidenceKey);
|
const evidence = useFragment(evidenceFragment, props.evidenceKey);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -253,9 +244,7 @@ function EvidenceRow(props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const evidenceUrl = props.snapshotId
|
const evidenceUrl = `/organizations/${props.organizationId}/measures/${props.measureId}/evidences/${evidence.id}`;
|
||||||
? `/organizations/${props.organizationId}/snapshots/${props.snapshotId}/risks/measures/${props.measureId}/evidences/${evidence.id}`
|
|
||||||
: `/organizations/${props.organizationId}/measures/${props.measureId}/evidences/${evidence.id}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -275,26 +264,24 @@ function EvidenceRow(props: {
|
|||||||
<Td>{fileSize(__, evidence.file?.size || 0)}</Td>
|
<Td>{fileSize(__, evidence.file?.size || 0)}</Td>
|
||||||
<Td>{formatDate(evidence.createdAt)}</Td>
|
<Td>{formatDate(evidence.createdAt)}</Td>
|
||||||
<Td noLink>
|
<Td noLink>
|
||||||
{!props.hideActions && (
|
<div className="flex gap-2">
|
||||||
<div className="flex gap-2">
|
<ActionDropdown>
|
||||||
<ActionDropdown>
|
<DropdownItem onClick={() => setIsDownloading(true)}>
|
||||||
<DropdownItem onClick={() => setIsDownloading(true)}>
|
<IconArrowInbox size={16} />
|
||||||
<IconArrowInbox size={16} />
|
{__("Download")}
|
||||||
{__("Download")}
|
</DropdownItem>
|
||||||
|
{evidence.canDelete && (
|
||||||
|
<DropdownItem
|
||||||
|
variant="danger"
|
||||||
|
icon={IconTrashCan}
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
{__("Delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{evidence.canDelete && (
|
)}
|
||||||
<DropdownItem
|
</ActionDropdown>
|
||||||
variant="danger"
|
</div>
|
||||||
icon={IconTrashCan}
|
|
||||||
onClick={handleDelete}
|
|
||||||
disabled={isDeleting}
|
|
||||||
>
|
|
||||||
{__("Delete")}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</ActionDropdown>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -12,11 +12,7 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import {
|
import { getTreatment, sprintf } from "@probo/helpers";
|
||||||
getTreatment,
|
|
||||||
sprintf,
|
|
||||||
validateSnapshotConsistency,
|
|
||||||
} from "@probo/helpers";
|
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +37,6 @@ import { Outlet, useNavigate, useParams } from "react-router";
|
|||||||
import { ConnectionHandler } from "relay-runtime";
|
import { ConnectionHandler } from "relay-runtime";
|
||||||
|
|
||||||
import type { RiskGraphNodeQuery } from "#/__generated__/core/RiskGraphNodeQuery.graphql";
|
import type { RiskGraphNodeQuery } from "#/__generated__/core/RiskGraphNodeQuery.graphql";
|
||||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
|
||||||
import {
|
import {
|
||||||
riskNodeQuery,
|
riskNodeQuery,
|
||||||
RisksConnectionKey,
|
RisksConnectionKey,
|
||||||
@@ -56,13 +51,11 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function RiskDetailPage(props: Props) {
|
export default function RiskDetailPage(props: Props) {
|
||||||
const { riskId, snapshotId } = useParams<{
|
const { riskId } = useParams<{
|
||||||
riskId: string;
|
riskId: string;
|
||||||
snapshotId?: string;
|
|
||||||
}>();
|
}>();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
|
|
||||||
if (!riskId) {
|
if (!riskId) {
|
||||||
throw new Error("Cannot load risk detail page without riskId parameter");
|
throw new Error("Cannot load risk detail page without riskId parameter");
|
||||||
@@ -74,7 +67,6 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
props.queryRef,
|
props.queryRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
validateSnapshotConsistency(risk, snapshotId);
|
|
||||||
const [deleteRisk] = useDeleteRiskMutation();
|
const [deleteRisk] = useDeleteRiskMutation();
|
||||||
|
|
||||||
usePageTitle(risk.name ?? "Risk detail");
|
usePageTitle(risk.name ?? "Risk detail");
|
||||||
@@ -84,7 +76,6 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
const connectionId = ConnectionHandler.getConnectionID(
|
const connectionId = ConnectionHandler.getConnectionID(
|
||||||
organizationId,
|
organizationId,
|
||||||
RisksConnectionKey,
|
RisksConnectionKey,
|
||||||
{ filter: { snapshotId: snapshotId || null } },
|
|
||||||
);
|
);
|
||||||
confirm(
|
confirm(
|
||||||
() =>
|
() =>
|
||||||
@@ -95,11 +86,7 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
connections: [connectionId],
|
connections: [connectionId],
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
const risksUrl
|
void navigate(`/organizations/${organizationId}/risks`);
|
||||||
= isSnapshotMode && snapshotId
|
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks`
|
|
||||||
: `/organizations/${organizationId}/risks`;
|
|
||||||
void navigate(risksUrl);
|
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -120,19 +107,11 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
const controlsCount = risk.controlsInfo?.totalCount ?? 0;
|
const controlsCount = risk.controlsInfo?.totalCount ?? 0;
|
||||||
const obligationsCount = risk.obligationsInfo?.totalCount ?? 0;
|
const obligationsCount = risk.obligationsInfo?.totalCount ?? 0;
|
||||||
|
|
||||||
const risksUrl
|
const risksUrl = `/organizations/${organizationId}/risks`;
|
||||||
= isSnapshotMode && snapshotId
|
const baseTabUrl = `/organizations/${organizationId}/risks/${riskId}`;
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks`
|
|
||||||
: `/organizations/${organizationId}/risks`;
|
|
||||||
|
|
||||||
const baseTabUrl
|
|
||||||
= isSnapshotMode && snapshotId
|
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/${riskId}`
|
|
||||||
: `/organizations/${organizationId}/risks/${riskId}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
@@ -146,56 +125,50 @@ export default function RiskDetailPage(props: Props) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!isSnapshotMode && (
|
<div className="flex gap-2">
|
||||||
<div className="flex gap-2">
|
{risk.canUpdate && (
|
||||||
{risk.canUpdate && (
|
<FormRiskDialog
|
||||||
<FormRiskDialog
|
trigger={(
|
||||||
trigger={(
|
<Button icon={IconPencil} variant="secondary">
|
||||||
<Button icon={IconPencil} variant="secondary">
|
{__("Edit")}
|
||||||
{__("Edit")}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
risk={{ id: riskId, ...risk }}
|
||||||
risk={{ id: riskId, ...risk }}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{risk.canDelete && (
|
||||||
{risk.canDelete && (
|
<ActionDropdown variant="secondary">
|
||||||
<ActionDropdown variant="secondary">
|
<DropdownItem
|
||||||
<DropdownItem
|
variant="danger"
|
||||||
variant="danger"
|
icon={IconTrashCan}
|
||||||
icon={IconTrashCan}
|
onClick={onDelete}
|
||||||
onClick={onDelete}
|
>
|
||||||
>
|
{__("Delete")}
|
||||||
{__("Delete")}
|
</DropdownItem>
|
||||||
</DropdownItem>
|
</ActionDropdown>
|
||||||
</ActionDropdown>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageHeader title={risk.name} description={risk.description} />
|
<PageHeader title={risk.name} description={risk.description} />
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabLink to={`${baseTabUrl}/overview`}>{__("Overview")}</TabLink>
|
<TabLink to={`${baseTabUrl}/overview`}>{__("Overview")}</TabLink>
|
||||||
{!isSnapshotMode && (
|
<TabLink to={`${baseTabUrl}/measures`}>
|
||||||
<>
|
{__("Measures")}
|
||||||
<TabLink to={`${baseTabUrl}/measures`}>
|
<TabBadge>{measuresCount}</TabBadge>
|
||||||
{__("Measures")}
|
</TabLink>
|
||||||
<TabBadge>{measuresCount}</TabBadge>
|
<TabLink to={`${baseTabUrl}/documents`}>
|
||||||
</TabLink>
|
{__("Documents")}
|
||||||
<TabLink to={`${baseTabUrl}/documents`}>
|
<TabBadge>{documentsCount}</TabBadge>
|
||||||
{__("Documents")}
|
</TabLink>
|
||||||
<TabBadge>{documentsCount}</TabBadge>
|
<TabLink to={`${baseTabUrl}/controls`}>
|
||||||
</TabLink>
|
{__("Controls")}
|
||||||
<TabLink to={`${baseTabUrl}/controls`}>
|
<TabBadge>{controlsCount}</TabBadge>
|
||||||
{__("Controls")}
|
</TabLink>
|
||||||
<TabBadge>{controlsCount}</TabBadge>
|
<TabLink to={`${baseTabUrl}/obligations`}>
|
||||||
</TabLink>
|
{__("Obligations")}
|
||||||
<TabLink to={`${baseTabUrl}/obligations`}>
|
<TabBadge>{obligationsCount}</TabBadge>
|
||||||
{__("Obligations")}
|
</TabLink>
|
||||||
<TabBadge>{obligationsCount}</TabBadge>
|
|
||||||
</TabLink>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Outlet context={{ risk }} />
|
<Outlet context={{ risk }} />
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ import {
|
|||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
Button,
|
Button,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
|
IconPageTextLine,
|
||||||
IconPencil,
|
IconPencil,
|
||||||
IconPlusLarge,
|
IconPlusLarge,
|
||||||
IconTrashCan,
|
IconTrashCan,
|
||||||
|
IconUpload,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
RisksChart,
|
RisksChart,
|
||||||
SeverityBadge,
|
SeverityBadge,
|
||||||
@@ -34,16 +36,16 @@ import {
|
|||||||
useDialogRef,
|
useDialogRef,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import type { PreloadedQuery } from "react-relay";
|
import type { PreloadedQuery } from "react-relay";
|
||||||
import { useParams } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import type { RiskGraphFragment$data } from "#/__generated__/core/RiskGraphFragment.graphql";
|
import type { RiskGraphFragment$data } from "#/__generated__/core/RiskGraphFragment.graphql";
|
||||||
import type { RiskGraphListQuery } from "#/__generated__/core/RiskGraphListQuery.graphql";
|
import type { RiskGraphListQuery } from "#/__generated__/core/RiskGraphListQuery.graphql";
|
||||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
|
||||||
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
||||||
import { useDeleteRiskMutation, useRisksQuery } from "#/hooks/graph/RiskGraph";
|
import { useDeleteRiskMutation, useRisksQuery } from "#/hooks/graph/RiskGraph";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
import type { NodeOf } from "#/types";
|
import type { NodeOf } from "#/types";
|
||||||
|
|
||||||
|
import { PublishRiskListDialog } from "./dialogs/PublishRiskListDialog";
|
||||||
import FormRiskDialog from "./FormRiskDialog";
|
import FormRiskDialog from "./FormRiskDialog";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -53,11 +55,10 @@ type Props = {
|
|||||||
export default function RisksPage(props: Props) {
|
export default function RisksPage(props: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
const navigate = useNavigate();
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { canCreateRisk },
|
data: { canCreateRisk, canPublishRisk, risksDocument },
|
||||||
connectionId,
|
connectionId,
|
||||||
risks,
|
risks,
|
||||||
...pagination
|
...pagination
|
||||||
@@ -70,7 +71,6 @@ export default function RisksPage(props: Props) {
|
|||||||
}) => {
|
}) => {
|
||||||
pagination.refetch(
|
pagination.refetch(
|
||||||
{
|
{
|
||||||
snapshotId,
|
|
||||||
order: {
|
order: {
|
||||||
direction: order.direction as "ASC" | "DESC",
|
direction: order.direction as "ASC" | "DESC",
|
||||||
field: order.field as
|
field: order.field as
|
||||||
@@ -90,27 +90,54 @@ export default function RisksPage(props: Props) {
|
|||||||
usePageTitle(__("Risks"));
|
usePageTitle(__("Risks"));
|
||||||
|
|
||||||
const hasAnyAction
|
const hasAnyAction
|
||||||
= !isSnapshotMode
|
= risks.some(({ canDelete, canUpdate }) => canUpdate || canDelete);
|
||||||
&& risks.some(({ canDelete, canUpdate }) => canUpdate || canDelete);
|
|
||||||
|
const defaultApproverIds
|
||||||
|
= risksDocument?.defaultApprovers?.map(a => a.id) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={__("Risks")}
|
title={__("Risks")}
|
||||||
description={__(
|
description={__(
|
||||||
"Risks are potential threats to your organization. Manage them by identifying, assessing, and implementing mitigation measures.",
|
"Risks are potential threats to your organization. Manage them by identifying, assessing, and implementing mitigation measures.",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && canCreateRisk && (
|
<div className="flex gap-2">
|
||||||
<FormRiskDialog
|
{risksDocument && (
|
||||||
connection={connectionId}
|
<Button
|
||||||
onSuccess={() => {
|
variant="secondary"
|
||||||
pagination.refetch({ snapshotId });
|
icon={IconPageTextLine}
|
||||||
}}
|
onClick={() => void navigate(
|
||||||
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
|
`/organizations/${organizationId}/documents/${risksDocument.id}`,
|
||||||
/>
|
)}
|
||||||
)}
|
>
|
||||||
|
{__("Document")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{canPublishRisk && (
|
||||||
|
<PublishRiskListDialog
|
||||||
|
organizationId={organizationId}
|
||||||
|
defaultApproverIds={defaultApproverIds}
|
||||||
|
onPublished={documentId => void navigate(
|
||||||
|
`/organizations/${organizationId}/documents/${documentId}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" icon={IconUpload}>
|
||||||
|
{__("Publish")}
|
||||||
|
</Button>
|
||||||
|
</PublishRiskListDialog>
|
||||||
|
)}
|
||||||
|
{canCreateRisk && (
|
||||||
|
<FormRiskDialog
|
||||||
|
connection={connectionId}
|
||||||
|
onSuccess={() => {
|
||||||
|
pagination.refetch({});
|
||||||
|
}}
|
||||||
|
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -167,8 +194,6 @@ type RowProps = {
|
|||||||
function RiskRow(props: RowProps) {
|
function RiskRow(props: RowProps) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { risk, connectionId, organizationId } = props;
|
const { risk, connectionId, organizationId } = props;
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
const [deleteRisk] = useDeleteRiskMutation();
|
const [deleteRisk] = useDeleteRiskMutation();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const onDelete = () => {
|
const onDelete = () => {
|
||||||
@@ -195,20 +220,15 @@ function RiskRow(props: RowProps) {
|
|||||||
};
|
};
|
||||||
const formDialogRef = useDialogRef();
|
const formDialogRef = useDialogRef();
|
||||||
|
|
||||||
const riskUrl
|
const riskUrl = `/organizations/${organizationId}/risks/${risk.id}/overview`;
|
||||||
= isSnapshotMode && snapshotId
|
|
||||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/risks/${risk.id}/overview`
|
|
||||||
: `/organizations/${organizationId}/risks/${risk.id}/overview`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isSnapshotMode && (
|
<FormRiskDialog
|
||||||
<FormRiskDialog
|
ref={formDialogRef}
|
||||||
ref={formDialogRef}
|
risk={risk}
|
||||||
risk={risk}
|
connection={connectionId}
|
||||||
connection={connectionId}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Tr to={riskUrl}>
|
<Tr to={riskUrl}>
|
||||||
<Td>{risk.name}</Td>
|
<Td>{risk.name}</Td>
|
||||||
<Td>{risk.category}</Td>
|
<Td>{risk.category}</Td>
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
IconSend,
|
||||||
|
IconUpload,
|
||||||
|
useDialogRef,
|
||||||
|
useToast,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useMutation } from "react-relay";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { PublishRiskListDialogMutation } from "#/__generated__/core/PublishRiskListDialogMutation.graphql";
|
||||||
|
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||||
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
|
|
||||||
|
const publishMutation = graphql`
|
||||||
|
mutation PublishRiskListDialogMutation(
|
||||||
|
$input: PublishRiskListInput!
|
||||||
|
) {
|
||||||
|
publishRiskList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
organizationId: string;
|
||||||
|
defaultApproverIds?: string[];
|
||||||
|
onPublished?: (documentId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PublishRiskListDialog({
|
||||||
|
children,
|
||||||
|
organizationId,
|
||||||
|
defaultApproverIds,
|
||||||
|
onPublished,
|
||||||
|
}: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
|
const schema = useMemo(() =>
|
||||||
|
z.object({
|
||||||
|
approverIds: z.array(z.string()),
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
} = useFormWithSchema(schema, {
|
||||||
|
defaultValues: {
|
||||||
|
approverIds: defaultApproverIds ?? [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [publish, isPublishing]
|
||||||
|
= useMutation<PublishRiskListDialogMutation>(publishMutation);
|
||||||
|
|
||||||
|
const approverIds = watch("approverIds");
|
||||||
|
const hasApprovers = approverIds.length > 0;
|
||||||
|
|
||||||
|
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||||
|
publish({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId,
|
||||||
|
approverIds: data.approverIds.length > 0 ? data.approverIds : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted(response) {
|
||||||
|
const documentId = response.publishRiskList?.documentEdge?.node?.id;
|
||||||
|
if (documentId) {
|
||||||
|
toast({
|
||||||
|
title: __("Success"),
|
||||||
|
description: hasApprovers
|
||||||
|
? __("Approval requested successfully.")
|
||||||
|
: __("Risks published successfully."),
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
dialogRef.current?.close();
|
||||||
|
reset();
|
||||||
|
onPublished?.(documentId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: __("Error"),
|
||||||
|
description: formatError(
|
||||||
|
__("Failed to publish risks"),
|
||||||
|
error as GraphQLError,
|
||||||
|
),
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="max-w-xl"
|
||||||
|
ref={dialogRef}
|
||||||
|
trigger={children}
|
||||||
|
title={__("Publish Risks")}
|
||||||
|
>
|
||||||
|
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||||
|
<DialogContent padded>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-txt-secondary">
|
||||||
|
{__("Select approvers to request approval before publishing, or publish directly without approvers.")}
|
||||||
|
</p>
|
||||||
|
<PeopleMultiSelectField
|
||||||
|
name="approverIds"
|
||||||
|
label={__("Approvers")}
|
||||||
|
control={control}
|
||||||
|
organizationId={organizationId}
|
||||||
|
placeholder={__("Add approvers...")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
icon={hasApprovers ? IconSend : IconUpload}
|
||||||
|
disabled={isPublishing}
|
||||||
|
>
|
||||||
|
{hasApprovers ? __("Request approval") : __("Publish")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import { getSnapshotTypeUrlPath } from "@probo/helpers";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
|
||||||
import { useNavigate, useParams } from "react-router";
|
|
||||||
|
|
||||||
import type { SnapshotGraphNodeQuery } from "#/__generated__/core/SnapshotGraphNodeQuery.graphql";
|
|
||||||
import { PageError } from "#/components/PageError";
|
|
||||||
import { snapshotNodeQuery } from "#/hooks/graph/SnapshotGraph";
|
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
queryRef: PreloadedQuery<SnapshotGraphNodeQuery>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SnapshotDetailPage({ queryRef }: Props) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const organizationId = useOrganizationId();
|
|
||||||
const { snapshotId } = useParams();
|
|
||||||
const data = usePreloadedQuery(snapshotNodeQuery, queryRef);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data.node || !data.node.type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snapshot = data.node;
|
|
||||||
const snapshotType = snapshot.type;
|
|
||||||
const urlPath = getSnapshotTypeUrlPath(snapshotType);
|
|
||||||
|
|
||||||
void navigate(
|
|
||||||
`/organizations/${organizationId}/snapshots/${snapshotId}${urlPath}`,
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}, [data.node, navigate, organizationId, snapshotId]);
|
|
||||||
|
|
||||||
if (!data.node || !data.node.type) {
|
|
||||||
return <PageError />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import {
|
|
||||||
formatDate,
|
|
||||||
getSnapshotTypeLabel,
|
|
||||||
getSnapshotTypeUrlPath,
|
|
||||||
} from "@probo/helpers";
|
|
||||||
import { usePageTitle } from "@probo/hooks";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import {
|
|
||||||
ActionDropdown,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
DropdownItem,
|
|
||||||
IconPlusLarge,
|
|
||||||
IconTrashCan,
|
|
||||||
PageHeader,
|
|
||||||
Table,
|
|
||||||
Tbody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
Thead,
|
|
||||||
Tr,
|
|
||||||
} from "@probo/ui";
|
|
||||||
import {
|
|
||||||
type PreloadedQuery,
|
|
||||||
useFragment,
|
|
||||||
usePreloadedQuery,
|
|
||||||
} from "react-relay";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
|
|
||||||
import type { SnapshotGraphListQuery } from "#/__generated__/core/SnapshotGraphListQuery.graphql";
|
|
||||||
import type {
|
|
||||||
SnapshotsPageFragment$data,
|
|
||||||
SnapshotsPageFragment$key,
|
|
||||||
} from "#/__generated__/core/SnapshotsPageFragment.graphql";
|
|
||||||
import { snapshotsQuery, useDeleteSnapshot } from "#/hooks/graph/SnapshotGraph";
|
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|
||||||
import type { NodeOf } from "#/types";
|
|
||||||
|
|
||||||
import SnapshotFormDialog from "./dialog/SnapshotFormDialog";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
queryRef: PreloadedQuery<SnapshotGraphListQuery>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const snapshotsFragment = graphql`
|
|
||||||
fragment SnapshotsPageFragment on Organization {
|
|
||||||
snapshots(first: 100)
|
|
||||||
@connection(key: "SnapshotsGraphListQuery__snapshots") {
|
|
||||||
__id
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
canDelete: permission(action: "core:snapshot:delete")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function SnapshotsPage(props: Props) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const organizationId = useOrganizationId();
|
|
||||||
const organization = usePreloadedQuery(
|
|
||||||
snapshotsQuery,
|
|
||||||
props.queryRef,
|
|
||||||
).organization;
|
|
||||||
const data = useFragment<SnapshotsPageFragment$key>(
|
|
||||||
snapshotsFragment,
|
|
||||||
organization,
|
|
||||||
);
|
|
||||||
const connectionId = data.snapshots.__id;
|
|
||||||
const snapshots = data.snapshots.edges.map(edge => edge.node);
|
|
||||||
usePageTitle(__("Snapshots"));
|
|
||||||
|
|
||||||
const hasAnyAction = snapshots.some(({ canDelete }) => canDelete);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<PageHeader
|
|
||||||
title={__("Snapshots")}
|
|
||||||
description={__(
|
|
||||||
"Snapshots capture point-in-time views of your organization's compliance state. Create snapshots to track progress over time.",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{organization.canCreateSnapshot && (
|
|
||||||
<SnapshotFormDialog connection={connectionId}>
|
|
||||||
<Button variant="primary" icon={IconPlusLarge}>
|
|
||||||
{__("New snapshot")}
|
|
||||||
</Button>
|
|
||||||
</SnapshotFormDialog>
|
|
||||||
)}
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
{snapshots.length > 0
|
|
||||||
? (
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th>{__("Name")}</Th>
|
|
||||||
<Th>{__("Type")}</Th>
|
|
||||||
<Th>{__("Description")}</Th>
|
|
||||||
<Th>{__("Created")}</Th>
|
|
||||||
{hasAnyAction && <Th></Th>}
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{snapshots.map(snapshot => (
|
|
||||||
<SnapshotRow
|
|
||||||
key={snapshot.id}
|
|
||||||
snapshot={snapshot}
|
|
||||||
connectionId={connectionId}
|
|
||||||
organizationId={organizationId}
|
|
||||||
hasAnyAction={hasAnyAction}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<h3 className="text-lg font-medium text-txt-secondary mb-2">
|
|
||||||
{__("No snapshots yet")}
|
|
||||||
</h3>
|
|
||||||
<p className="text-txt-tertiary mb-6">
|
|
||||||
{__("Create your first snapshot to get started")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshotRowProps = {
|
|
||||||
snapshot: NodeOf<SnapshotsPageFragment$data["snapshots"]>;
|
|
||||||
connectionId: string;
|
|
||||||
organizationId: string;
|
|
||||||
hasAnyAction: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function SnapshotRow(props: SnapshotRowProps) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const deleteSnapshot = useDeleteSnapshot(props.snapshot, props.connectionId);
|
|
||||||
const typePath = getSnapshotTypeUrlPath(props.snapshot.type);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr
|
|
||||||
to={`/organizations/${props.organizationId}/snapshots/${props.snapshot.id}${typePath}`}
|
|
||||||
>
|
|
||||||
<Td className="font-medium">{props.snapshot.name}</Td>
|
|
||||||
<Td>
|
|
||||||
<Badge variant="neutral">
|
|
||||||
{getSnapshotTypeLabel(__, props.snapshot.type)}
|
|
||||||
</Badge>
|
|
||||||
</Td>
|
|
||||||
<Td className="text-txt-secondary">
|
|
||||||
{props.snapshot.description || __("No description")}
|
|
||||||
</Td>
|
|
||||||
<Td className="text-txt-tertiary">
|
|
||||||
{formatDate(props.snapshot.createdAt)}
|
|
||||||
</Td>
|
|
||||||
{props.hasAnyAction && (
|
|
||||||
<Td noLink width={50} className="text-end">
|
|
||||||
<ActionDropdown>
|
|
||||||
{props.snapshot.canDelete && (
|
|
||||||
<DropdownItem
|
|
||||||
onClick={deleteSnapshot}
|
|
||||||
variant="danger"
|
|
||||||
icon={IconTrashCan}
|
|
||||||
>
|
|
||||||
{__("Delete")}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</ActionDropdown>
|
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import { snapshotTypes } from "@probo/helpers";
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
|
||||||
import { Breadcrumb,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
PropertyRow,
|
|
||||||
Textarea,
|
|
||||||
useDialogRef,
|
|
||||||
} from "@probo/ui";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { ControlledField } from "#/components/form/ControlledField";
|
|
||||||
import { SnapshotTypeOptions } from "#/components/form/SnapshotTypeOptions";
|
|
||||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
|
||||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|
||||||
|
|
||||||
const snapshotCreateMutation = graphql`
|
|
||||||
mutation SnapshotFormDialogCreateMutation(
|
|
||||||
$input: CreateSnapshotInput!
|
|
||||||
$connections: [ID!]!
|
|
||||||
) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge @prependEdge(connections: $connections) {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
canDelete: permission(action: "core:snapshot:delete")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const snapshotSchema = z.object({
|
|
||||||
name: z.string().min(2, { message: "Name is required" }),
|
|
||||||
description: z.string().optional(),
|
|
||||||
type: z.enum(snapshotTypes),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children?: ReactNode;
|
|
||||||
connection?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SnapshotFormDialog(props: Props) {
|
|
||||||
const { __ } = useTranslate();
|
|
||||||
const dialogRef = useDialogRef();
|
|
||||||
const organizationId = useOrganizationId();
|
|
||||||
const [mutate] = useMutationWithToasts(snapshotCreateMutation, {
|
|
||||||
successMessage: __("Snapshot created successfully."),
|
|
||||||
errorMessage: __("Failed to create snapshot"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleSubmit,
|
|
||||||
register,
|
|
||||||
reset,
|
|
||||||
control,
|
|
||||||
formState: { errors },
|
|
||||||
} = useFormWithSchema(snapshotSchema, {
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
type: "RISKS",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof snapshotSchema>) => {
|
|
||||||
await mutate({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
organizationId,
|
|
||||||
name: data.name,
|
|
||||||
description: data.description || undefined,
|
|
||||||
type: data.type,
|
|
||||||
},
|
|
||||||
connections: props.connection ? [props.connection] : [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
reset();
|
|
||||||
dialogRef.current?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
ref={dialogRef}
|
|
||||||
trigger={props.children}
|
|
||||||
title={<Breadcrumb items={[__("Snapshots"), __("New Snapshot")]} />}
|
|
||||||
>
|
|
||||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
|
||||||
<DialogContent className="grid grid-cols-[1fr_420px]">
|
|
||||||
<div className="py-8 px-10 space-y-4">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
required
|
|
||||||
variant="title"
|
|
||||||
placeholder={__("Snapshot name")}
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
variant="ghost"
|
|
||||||
autogrow
|
|
||||||
placeholder={__("Add description (optional)")}
|
|
||||||
{...register("description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Properties form */}
|
|
||||||
<div className="py-5 px-6 bg-subtle">
|
|
||||||
<Label>{__("Properties")}</Label>
|
|
||||||
|
|
||||||
<PropertyRow
|
|
||||||
id="type"
|
|
||||||
label={__("Type")}
|
|
||||||
error={errors.type?.message}
|
|
||||||
>
|
|
||||||
<ControlledField control={control} name="type" type="select">
|
|
||||||
<SnapshotTypeOptions />
|
|
||||||
</ControlledField>
|
|
||||||
</PropertyRow>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="submit">{__("Create snapshot")}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { useOutletContext, useParams } from "react-router";
|
import { useOutletContext } from "react-router";
|
||||||
|
|
||||||
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
||||||
import { useVendorForm } from "#/hooks/forms/useVendorForm";
|
import { useVendorForm } from "#/hooks/forms/useVendorForm";
|
||||||
@@ -44,13 +44,11 @@ export default function VendorCertificationsTab() {
|
|||||||
}>();
|
}>();
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { control, handleSubmit } = useVendorForm(vendor);
|
const { control, handleSubmit } = useVendorForm(vendor);
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
onSubmit={!isSnapshotMode && vendor.canUpdate
|
onSubmit={vendor.canUpdate
|
||||||
? e => void handleSubmit(e)
|
? e => void handleSubmit(e)
|
||||||
: undefined}
|
: undefined}
|
||||||
>
|
>
|
||||||
@@ -62,12 +60,12 @@ export default function VendorCertificationsTab() {
|
|||||||
<Certifications
|
<Certifications
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value ?? []}
|
value={field.value ?? []}
|
||||||
readOnly={isSnapshotMode || !vendor.canUpdate}
|
readOnly={!vendor.canUpdate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
{!isSnapshotMode && vendor.canUpdate && (
|
{vendor.canUpdate && (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">{__("Update vendor")}</Button>
|
<Button type="submit">{__("Update vendor")}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useFragment, useRefetchableFragment } from "react-relay";
|
import { useFragment, useRefetchableFragment } from "react-relay";
|
||||||
import { useOutletContext, useParams } from "react-router";
|
import { useOutletContext } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { ComplianceReportListQuery } from "#/__generated__/core/ComplianceReportListQuery.graphql";
|
import type { ComplianceReportListQuery } from "#/__generated__/core/ComplianceReportListQuery.graphql";
|
||||||
@@ -110,8 +110,6 @@ export default function VendorComplianceTab() {
|
|||||||
const connectionId = data.complianceReports.__id;
|
const connectionId = data.complianceReports.__id;
|
||||||
const reports = data.complianceReports.edges.map(edge => edge.node);
|
const reports = data.complianceReports.edges.map(edge => edge.node);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
usePageTitle(vendor.name + " - " + __("Compliance reports"));
|
usePageTitle(vendor.name + " - " + __("Compliance reports"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -120,7 +118,7 @@ export default function VendorComplianceTab() {
|
|||||||
title={__("Compliance reports")}
|
title={__("Compliance reports")}
|
||||||
description={__("Track vendor compliance certifications and reports.")}
|
description={__("Track vendor compliance certifications and reports.")}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && vendor.canUploadComplianceReport && (
|
{vendor.canUploadComplianceReport && (
|
||||||
<UploadComplianceReportDialog
|
<UploadComplianceReportDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
@@ -139,7 +137,7 @@ export default function VendorComplianceTab() {
|
|||||||
<SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh>
|
<SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh>
|
||||||
<Th>{__("Valid until")}</Th>
|
<Th>{__("Valid until")}</Th>
|
||||||
<Th>{__("File size")}</Th>
|
<Th>{__("File size")}</Th>
|
||||||
{!isSnapshotMode && reports.length > 0 && <Th>{__("Actions")}</Th>}
|
{reports.length > 0 && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -148,7 +146,6 @@ export default function VendorComplianceTab() {
|
|||||||
key={report.id}
|
key={report.id}
|
||||||
reportKey={report}
|
reportKey={report}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
isSnapshotMode={isSnapshotMode}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -160,7 +157,6 @@ export default function VendorComplianceTab() {
|
|||||||
type ReportRowProps = {
|
type ReportRowProps = {
|
||||||
reportKey: VendorComplianceTabFragment_report$key;
|
reportKey: VendorComplianceTabFragment_report$key;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
isSnapshotMode: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ReportRow(props: ReportRowProps) {
|
function ReportRow(props: ReportRowProps) {
|
||||||
@@ -203,33 +199,31 @@ function ReportRow(props: ReportRowProps) {
|
|||||||
<Td>{formatDate(report.reportDate)}</Td>
|
<Td>{formatDate(report.reportDate)}</Td>
|
||||||
<Td>{formatDate(report.validUntil)}</Td>
|
<Td>{formatDate(report.validUntil)}</Td>
|
||||||
<Td>{fileSize(__, report.file?.size ?? 0)}</Td>
|
<Td>{fileSize(__, report.file?.size ?? 0)}</Td>
|
||||||
{!props.isSnapshotMode && (
|
<Td width={50} className="text-end">
|
||||||
<Td width={50} className="text-end">
|
<ActionDropdown>
|
||||||
<ActionDropdown>
|
{report.file?.downloadUrl && (
|
||||||
{report.file?.downloadUrl && (
|
<DropdownItem
|
||||||
<DropdownItem
|
icon={IconArrowDown}
|
||||||
icon={IconArrowDown}
|
onClick={() =>
|
||||||
onClick={() =>
|
downloadFile(
|
||||||
downloadFile(
|
report.file!.downloadUrl,
|
||||||
report.file!.downloadUrl,
|
report.file!.fileName,
|
||||||
report.file!.fileName,
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{__("Download")}
|
||||||
{__("Download")}
|
</DropdownItem>
|
||||||
</DropdownItem>
|
)}
|
||||||
)}
|
{report.canDelete && (
|
||||||
{report.canDelete && (
|
<DropdownItem
|
||||||
<DropdownItem
|
icon={IconTrashCan}
|
||||||
icon={IconTrashCan}
|
onClick={handleDelete}
|
||||||
onClick={handleDelete}
|
variant="danger"
|
||||||
variant="danger"
|
>
|
||||||
>
|
{__("Delete")}
|
||||||
{__("Delete")}
|
</DropdownItem>
|
||||||
</DropdownItem>
|
)}
|
||||||
)}
|
</ActionDropdown>
|
||||||
</ActionDropdown>
|
</Td>
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ComponentProps, useState } from "react";
|
import { type ComponentProps, useState } from "react";
|
||||||
import { useFragment, useRefetchableFragment } from "react-relay";
|
import { useFragment, useRefetchableFragment } from "react-relay";
|
||||||
import { useOutletContext, useParams } from "react-router";
|
import { useOutletContext } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { VendorContactsListQuery } from "#/__generated__/core/VendorContactsListQuery.graphql";
|
import type { VendorContactsListQuery } from "#/__generated__/core/VendorContactsListQuery.graphql";
|
||||||
@@ -112,8 +112,6 @@ export default function VendorContactsTab() {
|
|||||||
const connectionId = data.contacts.__id;
|
const connectionId = data.contacts.__id;
|
||||||
const contacts = data.contacts.edges.map(edge => edge.node);
|
const contacts = data.contacts.edges.map(edge => edge.node);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
const [editingContact, setEditingContact]
|
const [editingContact, setEditingContact]
|
||||||
= useState<VendorContactsTabFragment_contact$data | null>(null);
|
= useState<VendorContactsTabFragment_contact$data | null>(null);
|
||||||
const hasAnyAction = contacts.some(
|
const hasAnyAction = contacts.some(
|
||||||
@@ -128,7 +126,7 @@ export default function VendorContactsTab() {
|
|||||||
title={__("Contacts")}
|
title={__("Contacts")}
|
||||||
description={__("Manage vendor contacts and their information.")}
|
description={__("Manage vendor contacts and their information.")}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && vendor.canCreateContact && (
|
{vendor.canCreateContact && (
|
||||||
<CreateContactDialog vendorId={vendor.id} connectionId={connectionId}>
|
<CreateContactDialog vendorId={vendor.id} connectionId={connectionId}>
|
||||||
<Button icon={IconPlusLarge}>{__("Add contact")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add contact")}</Button>
|
||||||
</CreateContactDialog>
|
</CreateContactDialog>
|
||||||
@@ -144,7 +142,7 @@ export default function VendorContactsTab() {
|
|||||||
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
|
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
|
||||||
<Th>{__("Phone")}</Th>
|
<Th>{__("Phone")}</Th>
|
||||||
<Th>{__("Role")}</Th>
|
<Th>{__("Role")}</Th>
|
||||||
{!isSnapshotMode && hasAnyAction && <Th>{__("Actions")}</Th>}
|
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -154,13 +152,12 @@ export default function VendorContactsTab() {
|
|||||||
contactKey={contact}
|
contactKey={contact}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
onEdit={setEditingContact}
|
onEdit={setEditingContact}
|
||||||
isSnapshotMode={isSnapshotMode}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</SortableTable>
|
</SortableTable>
|
||||||
|
|
||||||
{editingContact && !isSnapshotMode && editingContact.canUpdate && (
|
{editingContact && editingContact.canUpdate && (
|
||||||
<EditContactDialog
|
<EditContactDialog
|
||||||
contactId={editingContact.id}
|
contactId={editingContact.id}
|
||||||
contact={editingContact}
|
contact={editingContact}
|
||||||
@@ -175,7 +172,6 @@ type ContactRowProps = {
|
|||||||
contactKey: VendorContactsTabFragment_contact$key;
|
contactKey: VendorContactsTabFragment_contact$key;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
onEdit: (contact: VendorContactsTabFragment_contact$data) => void;
|
onEdit: (contact: VendorContactsTabFragment_contact$data) => void;
|
||||||
isSnapshotMode: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ContactRow(props: ContactRowProps) {
|
function ContactRow(props: ContactRowProps) {
|
||||||
@@ -245,7 +241,7 @@ function ContactRow(props: ContactRowProps) {
|
|||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{contact.role || __("—")}</Td>
|
<Td>{contact.role || __("—")}</Td>
|
||||||
{!props.isSnapshotMode && hasAnyAction && (
|
{hasAnyAction && (
|
||||||
<Td width={50} className="text-end">
|
<Td width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
{contact.canUpdate && (
|
{contact.canUpdate && (
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
import type { VendorCategory } from "@probo/vendors";
|
import type { VendorCategory } from "@probo/vendors";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { graphql, useFragment } from "react-relay";
|
import { graphql, useFragment } from "react-relay";
|
||||||
import { useOutletContext, useParams } from "react-router";
|
import { useOutletContext } from "react-router";
|
||||||
|
|
||||||
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
||||||
import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "#/__generated__/core/VendorOverviewTabBusinessAssociateAgreementFragment.graphql";
|
import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "#/__generated__/core/VendorOverviewTabBusinessAssociateAgreementFragment.graphql";
|
||||||
@@ -112,8 +112,6 @@ export default function VendorOverviewTab() {
|
|||||||
{ value: "VERSION_CONTROL", label: __("Version Control") },
|
{ value: "VERSION_CONTROL", label: __("Version Control") },
|
||||||
];
|
];
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -158,11 +156,11 @@ export default function VendorOverviewTab() {
|
|||||||
|
|
||||||
usePageTitle(vendor.name + " - " + __("Overview"));
|
usePageTitle(vendor.name + " - " + __("Overview"));
|
||||||
|
|
||||||
const isFormDisabled = isSubmitting || isSnapshotMode || !vendor.canUpdate;
|
const isFormDisabled = isSubmitting || !vendor.canUpdate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={isSnapshotMode || !vendor.canUpdate
|
onSubmit={!vendor.canUpdate
|
||||||
? undefined
|
? undefined
|
||||||
: e => void handleSubmit(e)}
|
: e => void handleSubmit(e)}
|
||||||
className="space-y-12"
|
className="space-y-12"
|
||||||
@@ -330,7 +328,7 @@ export default function VendorOverviewTab() {
|
|||||||
>
|
>
|
||||||
{__("Download PDF")}
|
{__("Download PDF")}
|
||||||
</Button>
|
</Button>
|
||||||
{!isSnapshotMode && businessAssociateAgreement.canUpdate && (
|
{businessAssociateAgreement.canUpdate && (
|
||||||
<EditBusinessAssociateAgreementDialog
|
<EditBusinessAssociateAgreementDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
agreement={{
|
agreement={{
|
||||||
@@ -342,7 +340,7 @@ export default function VendorOverviewTab() {
|
|||||||
<Button variant="quaternary" icon={IconPencil} />
|
<Button variant="quaternary" icon={IconPencil} />
|
||||||
</EditBusinessAssociateAgreementDialog>
|
</EditBusinessAssociateAgreementDialog>
|
||||||
)}
|
)}
|
||||||
{!isSnapshotMode && businessAssociateAgreement.canDelete && (
|
{businessAssociateAgreement.canDelete && (
|
||||||
<DeleteBusinessAssociateAgreementDialog
|
<DeleteBusinessAssociateAgreementDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
fileName={businessAssociateAgreement.fileName}
|
fileName={businessAssociateAgreement.fileName}
|
||||||
@@ -354,8 +352,7 @@ export default function VendorOverviewTab() {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
!isSnapshotMode
|
vendor.canUploadBAA && (
|
||||||
&& vendor.canUploadBAA && (
|
|
||||||
<UploadBusinessAssociateAgreementDialog
|
<UploadBusinessAssociateAgreementDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
onSuccess={() => window.location.reload()}
|
onSuccess={() => window.location.reload()}
|
||||||
@@ -405,7 +402,7 @@ export default function VendorOverviewTab() {
|
|||||||
>
|
>
|
||||||
{__("Download PDF")}
|
{__("Download PDF")}
|
||||||
</Button>
|
</Button>
|
||||||
{!isSnapshotMode && dataPrivacyAgreement.canUpdate && (
|
{dataPrivacyAgreement.canUpdate && (
|
||||||
<EditDataPrivacyAgreementDialog
|
<EditDataPrivacyAgreementDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
agreement={{
|
agreement={{
|
||||||
@@ -417,7 +414,7 @@ export default function VendorOverviewTab() {
|
|||||||
<Button variant="quaternary" icon={IconPencil} />
|
<Button variant="quaternary" icon={IconPencil} />
|
||||||
</EditDataPrivacyAgreementDialog>
|
</EditDataPrivacyAgreementDialog>
|
||||||
)}
|
)}
|
||||||
{!isSnapshotMode && dataPrivacyAgreement.canDelete && (
|
{dataPrivacyAgreement.canDelete && (
|
||||||
<DeleteDataPrivacyAgreementDialog
|
<DeleteDataPrivacyAgreementDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
fileName={dataPrivacyAgreement.fileName}
|
fileName={dataPrivacyAgreement.fileName}
|
||||||
@@ -429,8 +426,7 @@ export default function VendorOverviewTab() {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
!isSnapshotMode
|
vendor.canUploadDPA && (
|
||||||
&& vendor.canUploadDPA && (
|
|
||||||
<UploadDataPrivacyAgreementDialog
|
<UploadDataPrivacyAgreementDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
onSuccess={() => window.location.reload()}
|
onSuccess={() => window.location.reload()}
|
||||||
@@ -447,15 +443,13 @@ export default function VendorOverviewTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
{!isSnapshotMode && (
|
<div className="flex justify-end">
|
||||||
<div className="flex justify-end">
|
{vendor.canUpdate && (
|
||||||
{vendor.canUpdate && (
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
{__("Update vendor")}
|
||||||
{__("Update vendor")}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { type ComponentProps, useState } from "react";
|
import { type ComponentProps, useState } from "react";
|
||||||
import { useFragment, useRefetchableFragment } from "react-relay";
|
import { useFragment, useRefetchableFragment } from "react-relay";
|
||||||
import { useOutletContext, useParams } from "react-router";
|
import { useOutletContext } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
||||||
@@ -96,8 +96,6 @@ export default function VendorRiskAssessmentTab() {
|
|||||||
>(riskAssessmentsFragment, vendor);
|
>(riskAssessmentsFragment, vendor);
|
||||||
const assessments = data.riskAssessments.edges.map(edge => edge.node);
|
const assessments = data.riskAssessments.edges.map(edge => edge.node);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
const [expanded, setExpanded] = useState<string | null>(null);
|
const [expanded, setExpanded] = useState<string | null>(null);
|
||||||
|
|
||||||
usePageTitle(vendor.name + " - " + __("Risk Assessments"));
|
usePageTitle(vendor.name + " - " + __("Risk Assessments"));
|
||||||
@@ -106,7 +104,7 @@ export default function VendorRiskAssessmentTab() {
|
|||||||
return (
|
return (
|
||||||
<div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2">
|
<div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2">
|
||||||
{__("No risk assessments found")}
|
{__("No risk assessments found")}
|
||||||
{!isSnapshotMode && vendor.canCreateRiskAssessment && (
|
{vendor.canCreateRiskAssessment && (
|
||||||
<CreateRiskAssessmentDialog
|
<CreateRiskAssessmentDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
connection={data.riskAssessments.__id}
|
connection={data.riskAssessments.__id}
|
||||||
@@ -136,7 +134,7 @@ export default function VendorRiskAssessmentTab() {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{!isSnapshotMode && vendor.canCreateRiskAssessment && (
|
{vendor.canCreateRiskAssessment && (
|
||||||
<CreateRiskAssessmentDialog
|
<CreateRiskAssessmentDialog
|
||||||
vendorId={vendor.id}
|
vendorId={vendor.id}
|
||||||
connection={data.riskAssessments.__id}
|
connection={data.riskAssessments.__id}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ComponentProps, useState } from "react";
|
import { type ComponentProps, useState } from "react";
|
||||||
import { useFragment, useRefetchableFragment } from "react-relay";
|
import { useFragment, useRefetchableFragment } from "react-relay";
|
||||||
import { useOutletContext, useParams } from "react-router";
|
import { useOutletContext } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql";
|
||||||
@@ -110,8 +110,6 @@ export default function VendorServicesTab() {
|
|||||||
const connectionId = data.services.__id;
|
const connectionId = data.services.__id;
|
||||||
const services = data.services.edges.map(edge => edge.node);
|
const services = data.services.edges.map(edge => edge.node);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
|
||||||
const isSnapshotMode = Boolean(snapshotId);
|
|
||||||
const [editingService, setEditingService]
|
const [editingService, setEditingService]
|
||||||
= useState<VendorServicesTabFragment_service$data | null>(null);
|
= useState<VendorServicesTabFragment_service$data | null>(null);
|
||||||
const hasAnyAction = services.some(
|
const hasAnyAction = services.some(
|
||||||
@@ -126,7 +124,7 @@ export default function VendorServicesTab() {
|
|||||||
title={__("Services")}
|
title={__("Services")}
|
||||||
description={__("Manage services provided by this vendor.")}
|
description={__("Manage services provided by this vendor.")}
|
||||||
>
|
>
|
||||||
{!isSnapshotMode && vendor.canCreateService && (
|
{vendor.canCreateService && (
|
||||||
<CreateServiceDialog vendorId={vendor.id} connectionId={connectionId}>
|
<CreateServiceDialog vendorId={vendor.id} connectionId={connectionId}>
|
||||||
<Button icon={IconPlusLarge}>{__("Add service")}</Button>
|
<Button icon={IconPlusLarge}>{__("Add service")}</Button>
|
||||||
</CreateServiceDialog>
|
</CreateServiceDialog>
|
||||||
@@ -140,7 +138,7 @@ export default function VendorServicesTab() {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<SortableTh field="NAME">{__("Name")}</SortableTh>
|
<SortableTh field="NAME">{__("Name")}</SortableTh>
|
||||||
<Th>{__("Description")}</Th>
|
<Th>{__("Description")}</Th>
|
||||||
{!isSnapshotMode && hasAnyAction && <Th>{__("Actions")}</Th>}
|
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
@@ -150,13 +148,12 @@ export default function VendorServicesTab() {
|
|||||||
serviceKey={service}
|
serviceKey={service}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
onEdit={setEditingService}
|
onEdit={setEditingService}
|
||||||
isSnapshotMode={isSnapshotMode}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</SortableTable>
|
</SortableTable>
|
||||||
|
|
||||||
{editingService && !isSnapshotMode && editingService.canUpdate && (
|
{editingService && editingService.canUpdate && (
|
||||||
<EditServiceDialog
|
<EditServiceDialog
|
||||||
serviceId={editingService.id}
|
serviceId={editingService.id}
|
||||||
service={editingService}
|
service={editingService}
|
||||||
@@ -171,7 +168,6 @@ type ServiceRowProps = {
|
|||||||
serviceKey: VendorServicesTabFragment_service$key;
|
serviceKey: VendorServicesTabFragment_service$key;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
onEdit: (service: VendorServicesTabFragment_service$data) => void;
|
onEdit: (service: VendorServicesTabFragment_service$data) => void;
|
||||||
isSnapshotMode: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ServiceRow(props: ServiceRowProps) {
|
function ServiceRow(props: ServiceRowProps) {
|
||||||
@@ -213,7 +209,7 @@ function ServiceRow(props: ServiceRowProps) {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Td>{service.name}</Td>
|
<Td>{service.name}</Td>
|
||||||
<Td>{service.description || __("—")}</Td>
|
<Td>{service.description || __("—")}</Td>
|
||||||
{!props.isSnapshotMode && hasAnyAction && (
|
{hasAnyAction && (
|
||||||
<Td width={50} className="text-end">
|
<Td width={50} className="text-end">
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
{service.canUpdate && (
|
{service.canUpdate && (
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import { obligationRoutes } from "./routes/obligationRoutes";
|
|||||||
import { processingActivityRoutes } from "./routes/processingActivityRoutes";
|
import { processingActivityRoutes } from "./routes/processingActivityRoutes";
|
||||||
import { rightsRequestRoutes } from "./routes/rightsRequestRoutes";
|
import { rightsRequestRoutes } from "./routes/rightsRequestRoutes";
|
||||||
import { riskRoutes } from "./routes/riskRoutes";
|
import { riskRoutes } from "./routes/riskRoutes";
|
||||||
import { snapshotsRoutes } from "./routes/snapshotsRoutes";
|
|
||||||
import { statementsOfApplicabilityRoutes } from "./routes/statementsOfApplicabilityRoutes";
|
import { statementsOfApplicabilityRoutes } from "./routes/statementsOfApplicabilityRoutes";
|
||||||
import { taskRoutes } from "./routes/taskRoutes";
|
import { taskRoutes } from "./routes/taskRoutes";
|
||||||
import { vendorRoutes } from "./routes/vendorRoutes";
|
import { vendorRoutes } from "./routes/vendorRoutes";
|
||||||
@@ -307,7 +306,6 @@ const routes = [
|
|||||||
...accessReviewRoutes,
|
...accessReviewRoutes,
|
||||||
...compliancePageRoutes,
|
...compliancePageRoutes,
|
||||||
...cookieBannerRoutes,
|
...cookieBannerRoutes,
|
||||||
...snapshotsRoutes,
|
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
Component: PageError,
|
Component: PageError,
|
||||||
|
|||||||
@@ -37,20 +37,6 @@ export const riskRoutes = [
|
|||||||
loader: loaderFromQueryLoader(({ organizationId }) =>
|
loader: loaderFromQueryLoader(({ organizationId }) =>
|
||||||
loadQuery<RiskGraphListQuery>(coreEnvironment, risksQuery, {
|
loadQuery<RiskGraphListQuery>(coreEnvironment, risksQuery, {
|
||||||
organizationId: organizationId,
|
organizationId: organizationId,
|
||||||
snapshotId: null,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Component: withQueryRef(
|
|
||||||
lazy(() => import("#/pages/organizations/risks/RisksPage")),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "snapshots/:snapshotId/risks",
|
|
||||||
Fallback: RisksPageSkeleton,
|
|
||||||
loader: loaderFromQueryLoader(({ organizationId, snapshotId }) =>
|
|
||||||
loadQuery<RiskGraphListQuery>(coreEnvironment, risksQuery, {
|
|
||||||
organizationId: organizationId,
|
|
||||||
snapshotId: snapshotId,
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Component: withQueryRef(
|
Component: withQueryRef(
|
||||||
@@ -115,33 +101,4 @@ export const riskRoutes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "snapshots/:snapshotId/risks/:riskId",
|
|
||||||
Fallback: PageSkeleton,
|
|
||||||
loader: loaderFromQueryLoader(({ riskId }) =>
|
|
||||||
loadQuery<RiskGraphNodeQuery>(coreEnvironment, riskNodeQuery, {
|
|
||||||
riskId: riskId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Component: withQueryRef(
|
|
||||||
lazy(() => import("#/pages/organizations/risks/RiskDetailPage")),
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
loader: () => {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
throw redirect("overview");
|
|
||||||
},
|
|
||||||
Component: Fragment,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "overview",
|
|
||||||
Fallback: LinkCardSkeleton,
|
|
||||||
Component: lazy(
|
|
||||||
() => import("#/pages/organizations/risks/tabs/RiskOverviewTab"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] satisfies AppRoute[];
|
] satisfies AppRoute[];
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import { lazy } from "@probo/react-lazy";
|
|
||||||
import {
|
|
||||||
type AppRoute,
|
|
||||||
loaderFromQueryLoader,
|
|
||||||
withQueryRef,
|
|
||||||
} from "@probo/routes";
|
|
||||||
import { loadQuery } from "react-relay";
|
|
||||||
|
|
||||||
import type { SnapshotGraphListQuery } from "#/__generated__/core/SnapshotGraphListQuery.graphql";
|
|
||||||
import type { SnapshotGraphNodeQuery } from "#/__generated__/core/SnapshotGraphNodeQuery.graphql";
|
|
||||||
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
|
||||||
import { coreEnvironment } from "#/environments";
|
|
||||||
import { snapshotNodeQuery, snapshotsQuery } from "#/hooks/graph/SnapshotGraph";
|
|
||||||
|
|
||||||
export const snapshotsRoutes = [
|
|
||||||
{
|
|
||||||
path: "snapshots",
|
|
||||||
Fallback: PageSkeleton,
|
|
||||||
loader: loaderFromQueryLoader(({ organizationId }) =>
|
|
||||||
loadQuery<SnapshotGraphListQuery>(coreEnvironment, snapshotsQuery, {
|
|
||||||
organizationId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Component: withQueryRef(
|
|
||||||
lazy(() => import("#/pages/organizations/snapshots/SnapshotsPage")),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "snapshots/:snapshotId",
|
|
||||||
Fallback: PageSkeleton,
|
|
||||||
loader: loaderFromQueryLoader(({ snapshotId }) =>
|
|
||||||
loadQuery<SnapshotGraphNodeQuery>(coreEnvironment, snapshotNodeQuery, {
|
|
||||||
snapshotId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Component: withQueryRef(
|
|
||||||
lazy(() => import("#/pages/organizations/snapshots/SnapshotDetailPage")),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
] satisfies AppRoute[];
|
|
||||||
@@ -192,7 +192,7 @@ INSERT INTO document_versions (
|
|||||||
"tenant_id": org.tenantID,
|
"tenant_id": org.tenantID,
|
||||||
"organization_id": org.organizationID,
|
"organization_id": org.organizationID,
|
||||||
"document_id": documentID,
|
"document_id": documentID,
|
||||||
"title": "Asset List",
|
"title": "Assets",
|
||||||
"major": major + 1,
|
"major": major + 1,
|
||||||
"content": content,
|
"content": content,
|
||||||
"published_at": snap.publishedAt,
|
"published_at": snap.publishedAt,
|
||||||
@@ -386,7 +386,7 @@ ORDER BY v.name ASC;
|
|||||||
}
|
}
|
||||||
|
|
||||||
docData := docgen.AssetListData{
|
docData := docgen.AssetListData{
|
||||||
Title: "Asset List",
|
Title: "Assets",
|
||||||
OrganizationName: orgName,
|
OrganizationName: orgName,
|
||||||
CreatedAt: publishedAt,
|
CreatedAt: publishedAt,
|
||||||
TotalAssets: len(assetRows),
|
TotalAssets: len(assetRows),
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ INSERT INTO document_versions (
|
|||||||
"tenant_id": org.tenantID,
|
"tenant_id": org.tenantID,
|
||||||
"organization_id": org.organizationID,
|
"organization_id": org.organizationID,
|
||||||
"document_id": documentID,
|
"document_id": documentID,
|
||||||
"title": "Data List",
|
"title": "Data",
|
||||||
"major": major + 1,
|
"major": major + 1,
|
||||||
"content": content,
|
"content": content,
|
||||||
"published_at": snap.publishedAt,
|
"published_at": snap.publishedAt,
|
||||||
@@ -381,7 +381,7 @@ ORDER BY v.name ASC;
|
|||||||
}
|
}
|
||||||
|
|
||||||
docData := docgen.DataListData{
|
docData := docgen.DataListData{
|
||||||
Title: "Data List",
|
Title: "Data",
|
||||||
OrganizationName: orgName,
|
OrganizationName: orgName,
|
||||||
CreatedAt: publishedAt,
|
CreatedAt: publishedAt,
|
||||||
TotalData: len(dataRows),
|
TotalData: len(dataRows),
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ INSERT INTO document_versions (
|
|||||||
"tenant_id": org.tenantID,
|
"tenant_id": org.tenantID,
|
||||||
"organization_id": org.organizationID,
|
"organization_id": org.organizationID,
|
||||||
"document_id": documentID,
|
"document_id": documentID,
|
||||||
"title": "Finding List",
|
"title": "Findings",
|
||||||
"major": major + 1,
|
"major": major + 1,
|
||||||
"content": content,
|
"content": content,
|
||||||
"published_at": snap.publishedAt,
|
"published_at": snap.publishedAt,
|
||||||
@@ -403,7 +403,7 @@ ORDER BY f.reference_id ASC;
|
|||||||
}
|
}
|
||||||
|
|
||||||
docData := docgen.FindingListData{
|
docData := docgen.FindingListData{
|
||||||
Title: "Finding List",
|
Title: "Findings",
|
||||||
OrganizationName: orgName,
|
OrganizationName: orgName,
|
||||||
CreatedAt: publishedAt,
|
CreatedAt: publishedAt,
|
||||||
TotalFindings: len(findingRows),
|
TotalFindings: len(findingRows),
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ INSERT INTO document_versions (
|
|||||||
"tenant_id": org.tenantID,
|
"tenant_id": org.tenantID,
|
||||||
"organization_id": org.organizationID,
|
"organization_id": org.organizationID,
|
||||||
"document_id": documentID,
|
"document_id": documentID,
|
||||||
"title": "Obligation List",
|
"title": "Obligations",
|
||||||
"major": major + 1,
|
"major": major + 1,
|
||||||
"content": content,
|
"content": content,
|
||||||
"published_at": snap.publishedAt,
|
"published_at": snap.publishedAt,
|
||||||
@@ -389,7 +389,7 @@ ORDER BY ob.created_at ASC;
|
|||||||
}
|
}
|
||||||
|
|
||||||
docData := docgen.ObligationListData{
|
docData := docgen.ObligationListData{
|
||||||
Title: "Obligation List",
|
Title: "Obligations",
|
||||||
OrganizationName: orgName,
|
OrganizationName: orgName,
|
||||||
CreatedAt: publishedAt,
|
CreatedAt: publishedAt,
|
||||||
TotalObligations: len(obligationRows),
|
TotalObligations: len(obligationRows),
|
||||||
|
|||||||
525
cmd/migrate-risk-snapshots-to-documents/main.go
Normal file
525
cmd/migrate-risk-snapshots-to-documents/main.go
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// Command migrate-risk-snapshots-to-documents creates documents and document
|
||||||
|
// versions from existing risk snapshots. For each organization that has risk
|
||||||
|
// snapshots, it generates a risk list document using the same ProseMirror
|
||||||
|
// builder as the publish flow.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/docgen"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/probo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
var (
|
||||||
|
pgDSN string
|
||||||
|
dryRun bool
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(
|
||||||
|
&pgDSN,
|
||||||
|
"pg-dsn",
|
||||||
|
os.Getenv("DATABASE_URL"),
|
||||||
|
"PostgreSQL connection URL (default: DATABASE_URL env)",
|
||||||
|
)
|
||||||
|
flag.BoolVar(&dryRun, "dry-run", false, "show what would be done without writing")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if pgDSN == "" {
|
||||||
|
return fmt.Errorf("set -pg-dsn or DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
pgClient, err := newPgClientFromDSN(pgDSN)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create pg client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrate(ctx, pgClient, dryRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
type orgWithRiskSnapshots struct {
|
||||||
|
organizationID gid.GID
|
||||||
|
tenantID gid.TenantID
|
||||||
|
organizationName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type riskSnapshot struct {
|
||||||
|
snapshotID string
|
||||||
|
publishedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error {
|
||||||
|
var orgs []orgWithRiskSnapshots
|
||||||
|
err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
var err error
|
||||||
|
orgs, err = loadOrgsWithRiskSnapshots(ctx, conn)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(orgs) == 0 {
|
||||||
|
fmt.Println("no organizations with risk snapshots to migrate")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats struct {
|
||||||
|
documents, versions, failed int
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, org := range orgs {
|
||||||
|
if dryRun {
|
||||||
|
var count int
|
||||||
|
err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
snapshots, err := loadRiskSnapshots(ctx, conn, org.organizationID)
|
||||||
|
count = len(snapshots)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("would migrate org %s (%s) — %d risk snapshot(s)\n",
|
||||||
|
org.organizationID, org.organizationName, count)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := pgClient.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
return migrateOrg(ctx, tx, org)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "FAIL org %s (%s): %v\n",
|
||||||
|
org.organizationID, org.organizationName, err)
|
||||||
|
stats.failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.documents++
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("\n%d organization(s) would be migrated\n", len(orgs))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nmigrated %d organization(s), %d failed\n",
|
||||||
|
stats.documents, stats.failed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateOrg(ctx context.Context, tx pg.Tx, org orgWithRiskSnapshots) error {
|
||||||
|
snapshots, err := loadRiskSnapshots(ctx, tx, org.organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(snapshots) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
documentID := gid.New(org.tenantID, coredata.DocumentEntityType)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO documents (
|
||||||
|
id, tenant_id, organization_id, write_mode,
|
||||||
|
current_published_major, current_published_minor,
|
||||||
|
trust_center_visibility, status, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
@id, @tenant_id, @organization_id,
|
||||||
|
'GENERATED'::document_write_mode,
|
||||||
|
@current_published_major, 0,
|
||||||
|
'NONE'::trust_center_visibility,
|
||||||
|
'ACTIVE'::document_status,
|
||||||
|
@created_at, @updated_at
|
||||||
|
)`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"id": documentID,
|
||||||
|
"tenant_id": org.tenantID,
|
||||||
|
"organization_id": org.organizationID,
|
||||||
|
"current_published_major": len(snapshots),
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`INSERT INTO generated_documents (organization_id, tenant_id, risks_document_id, created_at, updated_at)
|
||||||
|
VALUES (@organization_id, @tenant_id, @risks_document_id, @created_at, @updated_at)
|
||||||
|
ON CONFLICT (organization_id) DO UPDATE SET risks_document_id = @risks_document_id, updated_at = @updated_at`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"organization_id": org.organizationID,
|
||||||
|
"tenant_id": org.tenantID,
|
||||||
|
"risks_document_id": documentID,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot link document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for major, snap := range snapshots {
|
||||||
|
content, err := buildSnapshotContent(ctx, tx, snap.snapshotID, org.organizationName, snap.publishedAt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build content for snapshot %s: %w", snap.snapshotID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionID := gid.New(org.tenantID, coredata.DocumentVersionEntityType)
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO document_versions (
|
||||||
|
id, tenant_id, organization_id, document_id,
|
||||||
|
title, major, minor, classification, document_type,
|
||||||
|
content, changelog, status, orientation,
|
||||||
|
pdf_attempt_count,
|
||||||
|
published_at, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
@id, @tenant_id, @organization_id, @document_id,
|
||||||
|
@title, @major, 0,
|
||||||
|
'CONFIDENTIAL'::document_classification,
|
||||||
|
'REGISTER'::document_type,
|
||||||
|
@content, '',
|
||||||
|
'PUBLISHED'::document_version_status,
|
||||||
|
'PORTRAIT'::document_version_orientation,
|
||||||
|
0,
|
||||||
|
@published_at, @published_at, @published_at
|
||||||
|
)`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"id": versionID,
|
||||||
|
"tenant_id": org.tenantID,
|
||||||
|
"organization_id": org.organizationID,
|
||||||
|
"document_id": documentID,
|
||||||
|
"title": "Risks",
|
||||||
|
"major": major + 1,
|
||||||
|
"content": content,
|
||||||
|
"published_at": snap.publishedAt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot insert version for snapshot %s: %w", snap.snapshotID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("OK org %s (%s) — %d version(s)\n",
|
||||||
|
org.organizationID, org.organizationName, len(snapshots))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOrgsWithRiskSnapshots(ctx context.Context, conn pg.Querier) ([]orgWithRiskSnapshots, error) {
|
||||||
|
rows, err := conn.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT DISTINCT
|
||||||
|
o.id,
|
||||||
|
o.tenant_id,
|
||||||
|
o.name,
|
||||||
|
o.created_at
|
||||||
|
FROM organizations o
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM generated_documents gd
|
||||||
|
WHERE gd.organization_id = o.id AND gd.risks_document_id IS NOT NULL
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM snapshots s
|
||||||
|
WHERE s.organization_id = o.id AND s.type = 'RISKS'
|
||||||
|
)
|
||||||
|
ORDER BY o.created_at;
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot query organizations with risk snapshots: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []orgWithRiskSnapshots
|
||||||
|
for rows.Next() {
|
||||||
|
var o orgWithRiskSnapshots
|
||||||
|
var createdAt time.Time
|
||||||
|
if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan organization: %w", err)
|
||||||
|
}
|
||||||
|
result = append(result, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadRiskSnapshots(ctx context.Context, conn pg.Querier, organizationID gid.GID) ([]riskSnapshot, error) {
|
||||||
|
rows, err := conn.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT DISTINCT
|
||||||
|
s.id,
|
||||||
|
s.created_at
|
||||||
|
FROM snapshots s
|
||||||
|
WHERE s.organization_id = @organization_id
|
||||||
|
AND s.type = 'RISKS'
|
||||||
|
ORDER BY s.created_at ASC;
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{"organization_id": organizationID},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot query risk snapshots for org %s: %w", organizationID, err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []riskSnapshot
|
||||||
|
for rows.Next() {
|
||||||
|
var s riskSnapshot
|
||||||
|
if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan snapshot: %w", err)
|
||||||
|
}
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
type riskInfo struct {
|
||||||
|
id string
|
||||||
|
name string
|
||||||
|
description *string
|
||||||
|
category string
|
||||||
|
treatment string
|
||||||
|
note string
|
||||||
|
ownerName string
|
||||||
|
inherentLikelihood int
|
||||||
|
inherentImpact int
|
||||||
|
inherentRiskScore int
|
||||||
|
residualLikelihood int
|
||||||
|
residualImpact int
|
||||||
|
residualRiskScore int
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSnapshotContent(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
snapshotID string,
|
||||||
|
orgName string,
|
||||||
|
publishedAt time.Time,
|
||||||
|
) (string, error) {
|
||||||
|
riskRows, err := tx.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.name,
|
||||||
|
r.description,
|
||||||
|
r.category,
|
||||||
|
r.treatment::text,
|
||||||
|
r.note,
|
||||||
|
COALESCE(NULLIF(p.full_name, ''), 'Not assigned'),
|
||||||
|
r.inherent_likelihood,
|
||||||
|
r.inherent_impact,
|
||||||
|
r.inherent_risk_score,
|
||||||
|
r.residual_likelihood,
|
||||||
|
r.residual_impact,
|
||||||
|
r.residual_risk_score
|
||||||
|
FROM risks r
|
||||||
|
LEFT JOIN iam_membership_profiles p ON p.id = r.owner_profile_id
|
||||||
|
WHERE r.snapshot_id = @snapshot_id
|
||||||
|
ORDER BY r.name ASC;
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{"snapshot_id": snapshotID},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot load snapshot risks: %w", err)
|
||||||
|
}
|
||||||
|
defer riskRows.Close()
|
||||||
|
|
||||||
|
var risks []riskInfo
|
||||||
|
for riskRows.Next() {
|
||||||
|
var r riskInfo
|
||||||
|
if err := riskRows.Scan(
|
||||||
|
&r.id, &r.name, &r.description, &r.category, &r.treatment, &r.note,
|
||||||
|
&r.ownerName,
|
||||||
|
&r.inherentLikelihood, &r.inherentImpact, &r.inherentRiskScore,
|
||||||
|
&r.residualLikelihood, &r.residualImpact, &r.residualRiskScore,
|
||||||
|
); err != nil {
|
||||||
|
return "", fmt.Errorf("cannot scan risk: %w", err)
|
||||||
|
}
|
||||||
|
risks = append(risks, r)
|
||||||
|
}
|
||||||
|
if err := riskRows.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]docgen.RiskListRow, 0, len(risks))
|
||||||
|
for _, r := range risks {
|
||||||
|
row := docgen.RiskListRow{
|
||||||
|
Name: r.name,
|
||||||
|
Description: derefOrNotSpecified(r.description),
|
||||||
|
Category: stringOrNotSpecified(r.category),
|
||||||
|
Treatment: formatTreatment(r.treatment),
|
||||||
|
Owner: r.ownerName,
|
||||||
|
InherentLikelihood: r.inherentLikelihood,
|
||||||
|
InherentLikelihoodLabel: riskLikelihoodLabel(r.inherentLikelihood),
|
||||||
|
InherentImpact: r.inherentImpact,
|
||||||
|
InherentImpactLabel: riskImpactLabel(r.inherentImpact),
|
||||||
|
InherentRiskScore: r.inherentRiskScore,
|
||||||
|
InherentSeverity: riskSeverityLabel(r.inherentRiskScore),
|
||||||
|
ResidualLikelihood: r.residualLikelihood,
|
||||||
|
ResidualLikelihoodLabel: riskLikelihoodLabel(r.residualLikelihood),
|
||||||
|
ResidualImpact: r.residualImpact,
|
||||||
|
ResidualImpactLabel: riskImpactLabel(r.residualImpact),
|
||||||
|
ResidualRiskScore: r.residualRiskScore,
|
||||||
|
ResidualSeverity: riskSeverityLabel(r.residualRiskScore),
|
||||||
|
Note: stringOrNotSpecified(r.note),
|
||||||
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
docData := docgen.RiskListData{
|
||||||
|
Title: "Risks",
|
||||||
|
OrganizationName: orgName,
|
||||||
|
CreatedAt: publishedAt,
|
||||||
|
TotalRisks: len(rows),
|
||||||
|
Rows: rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
return probo.BuildRiskListDocument(docData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func derefOrNotSpecified(s *string) string {
|
||||||
|
if s == nil || *s == "" {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return *s
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringOrNotSpecified(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return "Not specified"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTreatment(t string) string {
|
||||||
|
switch t {
|
||||||
|
case "MITIGATED":
|
||||||
|
return "Mitigated"
|
||||||
|
case "ACCEPTED":
|
||||||
|
return "Accepted"
|
||||||
|
case "AVOIDED":
|
||||||
|
return "Avoided"
|
||||||
|
case "TRANSFERRED":
|
||||||
|
return "Transferred"
|
||||||
|
default:
|
||||||
|
return stringOrNotSpecified(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskLikelihoodLabel(v int) string {
|
||||||
|
switch v {
|
||||||
|
case 1:
|
||||||
|
return "Improbable"
|
||||||
|
case 2:
|
||||||
|
return "Remote"
|
||||||
|
case 3:
|
||||||
|
return "Occasional"
|
||||||
|
case 4:
|
||||||
|
return "Probable"
|
||||||
|
case 5:
|
||||||
|
return "Frequent"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskImpactLabel(v int) string {
|
||||||
|
switch v {
|
||||||
|
case 1:
|
||||||
|
return "Negligible"
|
||||||
|
case 2:
|
||||||
|
return "Low"
|
||||||
|
case 3:
|
||||||
|
return "Moderate"
|
||||||
|
case 4:
|
||||||
|
return "Significant"
|
||||||
|
case 5:
|
||||||
|
return "Catastrophic"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskSeverityLabel(score int) string {
|
||||||
|
switch {
|
||||||
|
case score >= 15:
|
||||||
|
return "Critical"
|
||||||
|
case score >= 5:
|
||||||
|
return "High"
|
||||||
|
default:
|
||||||
|
return "Low"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPgClientFromDSN(dsn string) (*pg.Client, error) {
|
||||||
|
u, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse DSN")
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []pg.Option
|
||||||
|
|
||||||
|
if u.Host != "" {
|
||||||
|
opts = append(opts, pg.WithAddr(u.Host))
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.User != nil {
|
||||||
|
opts = append(opts, pg.WithUser(u.User.Username()))
|
||||||
|
if password, ok := u.User.Password(); ok {
|
||||||
|
opts = append(opts, pg.WithPassword(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Path) > 1 {
|
||||||
|
opts = append(opts, pg.WithDatabase(u.Path[1:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pg.NewClient(opts...)
|
||||||
|
}
|
||||||
@@ -582,144 +582,6 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestControlSnapshotMapping_CreateDelete(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
||||||
|
|
||||||
// Create a framework and control
|
|
||||||
var createFrameworkResult struct {
|
|
||||||
CreateFramework struct {
|
|
||||||
FrameworkEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"frameworkEdge"`
|
|
||||||
} `json:"createFramework"`
|
|
||||||
}
|
|
||||||
err := owner.Execute(`
|
|
||||||
mutation($input: CreateFrameworkInput!) {
|
|
||||||
createFramework(input: $input) {
|
|
||||||
frameworkEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"organizationId": owner.GetOrganizationID().String(),
|
|
||||||
"name": "Framework for ControlSnapshot Mapping",
|
|
||||||
},
|
|
||||||
}, &createFrameworkResult)
|
|
||||||
require.NoError(t, err)
|
|
||||||
frameworkID := createFrameworkResult.CreateFramework.FrameworkEdge.Node.ID
|
|
||||||
|
|
||||||
var createControlResult struct {
|
|
||||||
CreateControl struct {
|
|
||||||
ControlEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"controlEdge"`
|
|
||||||
} `json:"createControl"`
|
|
||||||
}
|
|
||||||
err = owner.Execute(`
|
|
||||||
mutation($input: CreateControlInput!) {
|
|
||||||
createControl(input: $input) {
|
|
||||||
controlEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"frameworkId": frameworkID,
|
|
||||||
"name": "Control for Snapshot Mapping",
|
|
||||||
"description": "Test control",
|
|
||||||
"sectionTitle": "Section 1",
|
|
||||||
"bestPractice": true,
|
|
||||||
"maturityLevel": "INITIAL",
|
|
||||||
},
|
|
||||||
}, &createControlResult)
|
|
||||||
require.NoError(t, err)
|
|
||||||
controlID := createControlResult.CreateControl.ControlEdge.Node.ID
|
|
||||||
|
|
||||||
// Create a snapshot
|
|
||||||
var createSnapshotResult struct {
|
|
||||||
CreateSnapshot struct {
|
|
||||||
SnapshotEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"snapshotEdge"`
|
|
||||||
} `json:"createSnapshot"`
|
|
||||||
}
|
|
||||||
err = owner.Execute(`
|
|
||||||
mutation($input: CreateSnapshotInput!) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"organizationId": owner.GetOrganizationID().String(),
|
|
||||||
"name": "Snapshot for Control Mapping",
|
|
||||||
"type": "RISKS",
|
|
||||||
},
|
|
||||||
}, &createSnapshotResult)
|
|
||||||
require.NoError(t, err)
|
|
||||||
snapshotID := createSnapshotResult.CreateSnapshot.SnapshotEdge.Node.ID
|
|
||||||
|
|
||||||
t.Run("create mapping", func(t *testing.T) {
|
|
||||||
_, err := owner.Do(`
|
|
||||||
mutation($input: CreateControlSnapshotMappingInput!) {
|
|
||||||
createControlSnapshotMapping(input: $input) {
|
|
||||||
controlEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"controlId": controlID,
|
|
||||||
"snapshotId": snapshotID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("delete mapping", func(t *testing.T) {
|
|
||||||
_, err := owner.Do(`
|
|
||||||
mutation($input: DeleteControlSnapshotMappingInput!) {
|
|
||||||
deleteControlSnapshotMapping(input: $input) {
|
|
||||||
deletedControlId
|
|
||||||
deletedSnapshotId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"controlId": controlID,
|
|
||||||
"snapshotId": snapshotID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRiskDocumentMapping_CreateDelete(t *testing.T) {
|
func TestRiskDocumentMapping_CreateDelete(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|||||||
336
e2e/console/risk_publish_test.go
Normal file
336
e2e/console/risk_publish_test.go
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package console_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.probo.inc/probo/e2e/internal/factory"
|
||||||
|
"go.probo.inc/probo/e2e/internal/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRisk_PublishRiskList(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"publish without approvers publishes immediately",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
factory.CreateRisk(owner, factory.Attrs{"name": "Test Risk"})
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishRiskListInput!) {
|
||||||
|
publishRiskList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
writeMode
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
documentType
|
||||||
|
status
|
||||||
|
major
|
||||||
|
minor
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
PublishRiskList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
WriteMode string `json:"writeMode"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
DocumentType string `json:"documentType"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
Minor int `json:"minor"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishRiskList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(
|
||||||
|
query,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&result,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
doc := result.PublishRiskList.DocumentEdge.Node
|
||||||
|
assert.NotEmpty(t, doc.ID)
|
||||||
|
assert.Equal(t, "GENERATED", doc.WriteMode)
|
||||||
|
assert.Equal(t, "ACTIVE", doc.Status)
|
||||||
|
|
||||||
|
ver := result.PublishRiskList.DocumentVersionEdge.Node
|
||||||
|
assert.NotEmpty(t, ver.ID)
|
||||||
|
assert.Equal(t, "REGISTER", ver.DocumentType)
|
||||||
|
assert.Equal(t, "PUBLISHED", ver.Status)
|
||||||
|
assert.Equal(t, 1, ver.Major)
|
||||||
|
assert.Equal(t, 0, ver.Minor)
|
||||||
|
assert.Contains(t, ver.Content, "Purpose")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"publish with approvers creates draft pending approval",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishRiskListInput!) {
|
||||||
|
publishRiskList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node { id writeMode }
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node { id status major }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
PublishRiskList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
WriteMode string `json:"writeMode"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishRiskList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(
|
||||||
|
query,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
"approverIds": []string{owner.GetProfileID().String()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&result,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
doc := result.PublishRiskList.DocumentEdge.Node
|
||||||
|
assert.NotEmpty(t, doc.ID)
|
||||||
|
assert.Equal(t, "GENERATED", doc.WriteMode)
|
||||||
|
|
||||||
|
ver := result.PublishRiskList.DocumentVersionEdge.Node
|
||||||
|
assert.NotEmpty(t, ver.ID)
|
||||||
|
assert.Equal(t, "PENDING_APPROVAL", ver.Status)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"second publish reuses document and bumps major version",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
factory.CreateRisk(owner, factory.Attrs{"name": "Reuse Risk"})
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishRiskListInput!) {
|
||||||
|
publishRiskList(input: $input) {
|
||||||
|
documentEdge { node { id } }
|
||||||
|
documentVersionEdge { node { id major } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var r1, r2 struct {
|
||||||
|
PublishRiskList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishRiskList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(query, input, &r1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = owner.Execute(query, input, &r2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
r1.PublishRiskList.DocumentEdge.Node.ID,
|
||||||
|
r2.PublishRiskList.DocumentEdge.Node.ID,
|
||||||
|
"should reuse same document",
|
||||||
|
)
|
||||||
|
assert.Equal(t, 1, r1.PublishRiskList.DocumentVersionEdge.Node.Major)
|
||||||
|
assert.Equal(t, 2, r2.PublishRiskList.DocumentVersionEdge.Node.Major)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"organization risksDocument links to published document",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
factory.CreateRisk(owner, factory.Attrs{"name": "Linked Risk"})
|
||||||
|
|
||||||
|
const publishQuery = `
|
||||||
|
mutation($input: PublishRiskListInput!) {
|
||||||
|
publishRiskList(input: $input) {
|
||||||
|
documentEdge { node { id } }
|
||||||
|
documentVersionEdge { node { id } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var publishResult struct {
|
||||||
|
PublishRiskList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishRiskList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(
|
||||||
|
publishQuery,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&publishResult,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
docID := publishResult.PublishRiskList.DocumentEdge.Node.ID
|
||||||
|
|
||||||
|
const orgQuery = `
|
||||||
|
query($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on Organization {
|
||||||
|
id
|
||||||
|
risksDocument { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var orgResult struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
RisksDocument *struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"risksDocument"`
|
||||||
|
} `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.Execute(
|
||||||
|
orgQuery,
|
||||||
|
map[string]any{"id": owner.GetOrganizationID()},
|
||||||
|
&orgResult,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, orgResult.Node.RisksDocument)
|
||||||
|
assert.Equal(t, docID, orgResult.Node.RisksDocument.ID)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRisk_PublishRiskList_RBAC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
|
||||||
|
|
||||||
|
factory.CreateRisk(owner, factory.Attrs{"name": "RBAC Risk"})
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: PublishRiskListInput!) {
|
||||||
|
publishRiskList(input: $input) {
|
||||||
|
documentEdge { node { id } }
|
||||||
|
documentVersionEdge { node { id } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
t.Run(
|
||||||
|
"viewer cannot publish risk list",
|
||||||
|
func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := viewer.ExecuteShouldFail(
|
||||||
|
query,
|
||||||
|
map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
testutil.RequireForbiddenError(t, err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package console_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"go.probo.inc/probo/e2e/internal/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSnapshot_Create(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
||||||
|
|
||||||
query := `
|
|
||||||
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
CreateSnapshot struct {
|
|
||||||
SnapshotEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"snapshotEdge"`
|
|
||||||
} `json:"createSnapshot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"organizationId": owner.GetOrganizationID().String(),
|
|
||||||
"name": "Q4 2024 Risk Snapshot",
|
|
||||||
"description": "Quarterly risk assessment snapshot",
|
|
||||||
"type": "RISKS",
|
|
||||||
},
|
|
||||||
}, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
snapshot := result.CreateSnapshot.SnapshotEdge.Node
|
|
||||||
assert.NotEmpty(t, snapshot.ID)
|
|
||||||
assert.Equal(t, "Q4 2024 Risk Snapshot", snapshot.Name)
|
|
||||||
assert.Equal(t, "Quarterly risk assessment snapshot", snapshot.Description)
|
|
||||||
assert.Equal(t, "RISKS", snapshot.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSnapshot_Delete(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
||||||
|
|
||||||
// Create a snapshot to delete
|
|
||||||
createQuery := `
|
|
||||||
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var createResult struct {
|
|
||||||
CreateSnapshot struct {
|
|
||||||
SnapshotEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"snapshotEdge"`
|
|
||||||
} `json:"createSnapshot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(createQuery, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"organizationId": owner.GetOrganizationID().String(),
|
|
||||||
"name": fmt.Sprintf("Snapshot to Delete %d", time.Now().UnixNano()),
|
|
||||||
"type": "RISKS",
|
|
||||||
},
|
|
||||||
}, &createResult)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
snapshotID := createResult.CreateSnapshot.SnapshotEdge.Node.ID
|
|
||||||
|
|
||||||
deleteQuery := `
|
|
||||||
mutation DeleteSnapshot($input: DeleteSnapshotInput!) {
|
|
||||||
deleteSnapshot(input: $input) {
|
|
||||||
deletedSnapshotId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var deleteResult struct {
|
|
||||||
DeleteSnapshot struct {
|
|
||||||
DeletedSnapshotID string `json:"deletedSnapshotId"`
|
|
||||||
} `json:"deleteSnapshot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err = owner.Execute(deleteQuery, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"snapshotId": snapshotID,
|
|
||||||
},
|
|
||||||
}, &deleteResult)
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, snapshotID, deleteResult.DeleteSnapshot.DeletedSnapshotID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSnapshot_List(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
||||||
|
|
||||||
// Create multiple snapshots
|
|
||||||
snapshotTypes := []string{"RISKS"}
|
|
||||||
for i, snapshotType := range snapshotTypes {
|
|
||||||
query := `
|
|
||||||
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
CreateSnapshot struct {
|
|
||||||
SnapshotEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"snapshotEdge"`
|
|
||||||
} `json:"createSnapshot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"organizationId": owner.GetOrganizationID().String(),
|
|
||||||
"name": fmt.Sprintf("Snapshot %d %d", i, time.Now().UnixNano()),
|
|
||||||
"type": snapshotType,
|
|
||||||
},
|
|
||||||
}, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `
|
|
||||||
query GetSnapshots($id: ID!) {
|
|
||||||
node(id: $id) {
|
|
||||||
... on Organization {
|
|
||||||
snapshots(first: 10) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
Node struct {
|
|
||||||
Snapshots struct {
|
|
||||||
Edges []struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"edges"`
|
|
||||||
TotalCount int `json:"totalCount"`
|
|
||||||
} `json:"snapshots"`
|
|
||||||
} `json:"node"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
|
||||||
"id": owner.GetOrganizationID().String(),
|
|
||||||
}, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.GreaterOrEqual(t, result.Node.Snapshots.TotalCount, len(snapshotTypes))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSnapshot_Types(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
||||||
|
|
||||||
snapshotTypes := []string{"RISKS"}
|
|
||||||
|
|
||||||
for _, snapshotType := range snapshotTypes {
|
|
||||||
t.Run(snapshotType, func(t *testing.T) {
|
|
||||||
query := `
|
|
||||||
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
CreateSnapshot struct {
|
|
||||||
SnapshotEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"snapshotEdge"`
|
|
||||||
} `json:"createSnapshot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"organizationId": owner.GetOrganizationID().String(),
|
|
||||||
"name": fmt.Sprintf("Snapshot Type %s %d", snapshotType, time.Now().UnixNano()),
|
|
||||||
"type": snapshotType,
|
|
||||||
},
|
|
||||||
}, &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, snapshotType, result.CreateSnapshot.SnapshotEdge.Node.Type)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package mcp_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"go.probo.inc/probo/e2e/internal/factory"
|
|
||||||
"go.probo.inc/probo/e2e/internal/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMCP_Snapshot(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
||||||
mc := testutil.NewMCPClient(t, owner)
|
|
||||||
orgID := owner.GetOrganizationID().String()
|
|
||||||
|
|
||||||
// Create a risk so the snapshot has data
|
|
||||||
factory.CreateRisk(owner)
|
|
||||||
|
|
||||||
// Take snapshot
|
|
||||||
var takeResult struct {
|
|
||||||
Snapshot struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"snapshot"`
|
|
||||||
}
|
|
||||||
mc.CallToolInto("takeSnapshot", map[string]any{
|
|
||||||
"organizationId": orgID,
|
|
||||||
"name": factory.SafeName("Snapshot"),
|
|
||||||
"snapshotsType": "RISKS",
|
|
||||||
}, &takeResult)
|
|
||||||
require.NotEmpty(t, takeResult.Snapshot.ID)
|
|
||||||
|
|
||||||
// Get
|
|
||||||
var getResult struct {
|
|
||||||
Snapshot struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"snapshot"`
|
|
||||||
}
|
|
||||||
mc.CallToolInto("getSnapshot", map[string]any{
|
|
||||||
"id": takeResult.Snapshot.ID,
|
|
||||||
}, &getResult)
|
|
||||||
assert.Equal(t, takeResult.Snapshot.ID, getResult.Snapshot.ID)
|
|
||||||
|
|
||||||
// List
|
|
||||||
var listResult struct {
|
|
||||||
Snapshots []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"snapshots"`
|
|
||||||
}
|
|
||||||
mc.CallToolInto("listSnapshots", map[string]any{
|
|
||||||
"organizationId": orgID,
|
|
||||||
}, &listResult)
|
|
||||||
assert.NotEmpty(t, listResult.Snapshots)
|
|
||||||
}
|
|
||||||
BIN
migrate-risk-snapshots-to-documents
Executable file
BIN
migrate-risk-snapshots-to-documents
Executable file
Binary file not shown.
@@ -57,12 +57,6 @@ export {
|
|||||||
type ControlMaturityLevel,
|
type ControlMaturityLevel,
|
||||||
} from "./controls";
|
} from "./controls";
|
||||||
export { getAssetTypeVariant } from "./assets";
|
export { getAssetTypeVariant } from "./assets";
|
||||||
export {
|
|
||||||
getSnapshotTypeLabel,
|
|
||||||
getSnapshotTypeUrlPath,
|
|
||||||
snapshotTypes,
|
|
||||||
validateSnapshotConsistency,
|
|
||||||
} from "./snapshots";
|
|
||||||
export {
|
export {
|
||||||
getAuditStateLabel,
|
getAuditStateLabel,
|
||||||
getAuditStateVariant,
|
getAuditStateVariant,
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
type Translator = (s: string) => string;
|
|
||||||
|
|
||||||
export const snapshotTypes = [
|
|
||||||
"RISKS",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export function getSnapshotTypeLabel(__: Translator, type: string | null | undefined) {
|
|
||||||
if (!type) {
|
|
||||||
return __("Unknown");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "RISKS":
|
|
||||||
return __("Risks");
|
|
||||||
case "VENDORS":
|
|
||||||
return __("Vendors");
|
|
||||||
case "PROCESSING_ACTIVITIES":
|
|
||||||
return __("Processing Activities");
|
|
||||||
default:
|
|
||||||
return __("Unknown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSnapshotTypeUrlPath(type?: string): string {
|
|
||||||
switch (type) {
|
|
||||||
case "RISKS":
|
|
||||||
return "/risks";
|
|
||||||
case "VENDORS":
|
|
||||||
return "/vendors";
|
|
||||||
case "PROCESSING_ACTIVITIES":
|
|
||||||
return "/processing-activities";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SnapshotableResource {
|
|
||||||
snapshotId?: string | null | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateSnapshotConsistency(
|
|
||||||
resource: SnapshotableResource | null | undefined,
|
|
||||||
urlSnapshotId?: string | null | undefined
|
|
||||||
): void {
|
|
||||||
if (resource && resource.snapshotId !== (urlSnapshotId ?? null)) {
|
|
||||||
throw new Error("PAGE_NOT_FOUND");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -191,11 +191,6 @@ export class Probo implements INodeType {
|
|||||||
value: 'risk',
|
value: 'risk',
|
||||||
description: 'Manage risks',
|
description: 'Manage risks',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Snapshot',
|
|
||||||
value: 'snapshot',
|
|
||||||
description: 'Manage snapshots',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Statement of Applicability',
|
name: 'Statement of Applicability',
|
||||||
value: 'statementOfApplicability',
|
value: 'statementOfApplicability',
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ import * as linkAuditOp from './linkAudit.operation';
|
|||||||
import * as unlinkAuditOp from './unlinkAudit.operation';
|
import * as unlinkAuditOp from './unlinkAudit.operation';
|
||||||
import * as linkObligationOp from './linkObligation.operation';
|
import * as linkObligationOp from './linkObligation.operation';
|
||||||
import * as unlinkObligationOp from './unlinkObligation.operation';
|
import * as unlinkObligationOp from './unlinkObligation.operation';
|
||||||
import * as linkSnapshotOp from './linkSnapshot.operation';
|
|
||||||
import * as unlinkSnapshotOp from './unlinkSnapshot.operation';
|
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
export const description: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
@@ -89,12 +87,6 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Link an obligation to a control',
|
description: 'Link an obligation to a control',
|
||||||
action: 'Link an obligation to a control',
|
action: 'Link an obligation to a control',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Link Snapshot',
|
|
||||||
value: 'linkSnapshot',
|
|
||||||
description: 'Link a snapshot to a control',
|
|
||||||
action: 'Link a snapshot to a control',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Unlink Audit',
|
name: 'Unlink Audit',
|
||||||
value: 'unlinkAudit',
|
value: 'unlinkAudit',
|
||||||
@@ -119,12 +111,6 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Unlink an obligation from a control',
|
description: 'Unlink an obligation from a control',
|
||||||
action: 'Unlink an obligation from a control',
|
action: 'Unlink an obligation from a control',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Unlink Snapshot',
|
|
||||||
value: 'unlinkSnapshot',
|
|
||||||
description: 'Unlink a snapshot from a control',
|
|
||||||
action: 'Unlink a snapshot from a control',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Update',
|
name: 'Update',
|
||||||
value: 'update',
|
value: 'update',
|
||||||
@@ -147,8 +133,6 @@ export const description: INodeProperties[] = [
|
|||||||
...unlinkAuditOp.description,
|
...unlinkAuditOp.description,
|
||||||
...linkObligationOp.description,
|
...linkObligationOp.description,
|
||||||
...unlinkObligationOp.description,
|
...unlinkObligationOp.description,
|
||||||
...linkSnapshotOp.description,
|
|
||||||
...unlinkSnapshotOp.description,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -165,6 +149,4 @@ export {
|
|||||||
unlinkAuditOp as unlinkAudit,
|
unlinkAuditOp as unlinkAudit,
|
||||||
linkObligationOp as linkObligation,
|
linkObligationOp as linkObligation,
|
||||||
unlinkObligationOp as unlinkObligation,
|
unlinkObligationOp as unlinkObligation,
|
||||||
linkSnapshotOp as linkSnapshot,
|
|
||||||
unlinkSnapshotOp as unlinkSnapshot,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
||||||
import { proboApiRequest } from '../../GenericFunctions';
|
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Control ID',
|
|
||||||
name: 'controlId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['control'],
|
|
||||||
operation: ['unlinkSnapshot'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the control',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Snapshot ID',
|
|
||||||
name: 'snapshotId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['control'],
|
|
||||||
operation: ['unlinkSnapshot'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the snapshot to unlink',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function execute(
|
|
||||||
this: IExecuteFunctions,
|
|
||||||
itemIndex: number,
|
|
||||||
): Promise<INodeExecutionData> {
|
|
||||||
const controlId = this.getNodeParameter('controlId', itemIndex) as string;
|
|
||||||
const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string;
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
mutation DeleteControlSnapshotMapping($input: DeleteControlSnapshotMappingInput!) {
|
|
||||||
deleteControlSnapshotMapping(input: $input) {
|
|
||||||
deletedControlId
|
|
||||||
deletedSnapshotId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const responseData = await proboApiRequest.call(this, query, { input: { controlId, snapshotId } });
|
|
||||||
|
|
||||||
return {
|
|
||||||
json: responseData,
|
|
||||||
pairedItem: { item: itemIndex },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,6 @@ import * as processingActivity from './processingActivity';
|
|||||||
import * as rightsRequest from './rightsRequest';
|
import * as rightsRequest from './rightsRequest';
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
import * as risk from './risk';
|
import * as risk from './risk';
|
||||||
import * as snapshot from './snapshot';
|
|
||||||
import * as statementOfApplicability from './statementOfApplicability';
|
import * as statementOfApplicability from './statementOfApplicability';
|
||||||
import * as task from './task';
|
import * as task from './task';
|
||||||
import * as tia from './tia';
|
import * as tia from './tia';
|
||||||
@@ -80,7 +79,6 @@ export const resources: Record<string, ResourceModule> = {
|
|||||||
rightsRequest: rightsRequest as ResourceModule,
|
rightsRequest: rightsRequest as ResourceModule,
|
||||||
user: user as ResourceModule,
|
user: user as ResourceModule,
|
||||||
risk: risk as ResourceModule,
|
risk: risk as ResourceModule,
|
||||||
snapshot: snapshot as ResourceModule,
|
|
||||||
statementOfApplicability: statementOfApplicability as ResourceModule,
|
statementOfApplicability: statementOfApplicability as ResourceModule,
|
||||||
task: task as ResourceModule,
|
task: task as ResourceModule,
|
||||||
tia: tia as ResourceModule,
|
tia: tia as ResourceModule,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import * as linkDocumentOp from './linkDocument.operation';
|
|||||||
import * as unlinkDocumentOp from './unlinkDocument.operation';
|
import * as unlinkDocumentOp from './unlinkDocument.operation';
|
||||||
import * as linkObligationOp from './linkObligation.operation';
|
import * as linkObligationOp from './linkObligation.operation';
|
||||||
import * as unlinkObligationOp from './unlinkObligation.operation';
|
import * as unlinkObligationOp from './unlinkObligation.operation';
|
||||||
|
import * as publishOp from './publish.operation';
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
export const description: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
@@ -79,6 +80,12 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Link an obligation to a risk',
|
description: 'Link an obligation to a risk',
|
||||||
action: 'Link an obligation to a risk',
|
action: 'Link an obligation to a risk',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Publish List',
|
||||||
|
value: 'publish',
|
||||||
|
description: 'Publish the risk register as a document version',
|
||||||
|
action: 'Publish the risk register',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Unlink Document',
|
name: 'Unlink Document',
|
||||||
value: 'unlinkDocument',
|
value: 'unlinkDocument',
|
||||||
@@ -117,6 +124,7 @@ export const description: INodeProperties[] = [
|
|||||||
...unlinkDocumentOp.description,
|
...unlinkDocumentOp.description,
|
||||||
...linkObligationOp.description,
|
...linkObligationOp.description,
|
||||||
...unlinkObligationOp.description,
|
...unlinkObligationOp.description,
|
||||||
|
...publishOp.description,
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -131,4 +139,5 @@ export {
|
|||||||
unlinkDocumentOp as unlinkDocument,
|
unlinkDocumentOp as unlinkDocument,
|
||||||
linkObligationOp as linkObligation,
|
linkObligationOp as linkObligation,
|
||||||
unlinkObligationOp as unlinkObligation,
|
unlinkObligationOp as unlinkObligation,
|
||||||
|
publishOp as publish,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -17,32 +17,31 @@ import { proboApiRequest } from '../../GenericFunctions';
|
|||||||
|
|
||||||
export const description: INodeProperties[] = [
|
export const description: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Control ID',
|
displayName: 'Organization ID',
|
||||||
name: 'controlId',
|
name: 'organizationId',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
resource: ['control'],
|
resource: ['risk'],
|
||||||
operation: ['linkSnapshot'],
|
operation: ['publish'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description: 'The ID of the control',
|
description: 'The ID of the organization whose risk list to publish',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Snapshot ID',
|
displayName: 'Approver IDs',
|
||||||
name: 'snapshotId',
|
name: 'approverIds',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
resource: ['control'],
|
resource: ['risk'],
|
||||||
operation: ['linkSnapshot'],
|
operation: ['publish'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description: 'The ID of the snapshot to link',
|
description: 'Comma-separated list of approver profile IDs',
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -50,29 +49,50 @@ export async function execute(
|
|||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
itemIndex: number,
|
itemIndex: number,
|
||||||
): Promise<INodeExecutionData> {
|
): Promise<INodeExecutionData> {
|
||||||
const controlId = this.getNodeParameter('controlId', itemIndex) as string;
|
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
|
||||||
const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string;
|
const approverIds = this.getNodeParameter('approverIds', itemIndex, '') as string;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
mutation CreateControlSnapshotMapping($input: CreateControlSnapshotMappingInput!) {
|
mutation PublishRiskList($input: PublishRiskListInput!) {
|
||||||
createControlSnapshotMapping(input: $input) {
|
publishRiskList(input: $input) {
|
||||||
controlEdge {
|
documentEdge {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
name
|
status
|
||||||
|
currentPublishedMajor
|
||||||
|
currentPublishedMinor
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snapshotEdge {
|
documentVersionEdge {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
name
|
title
|
||||||
|
major
|
||||||
|
minor
|
||||||
|
status
|
||||||
|
classification
|
||||||
|
documentType
|
||||||
|
publishedAt
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const responseData = await proboApiRequest.call(this, query, { input: { controlId, snapshotId } });
|
const input: Record<string, unknown> = { organizationId };
|
||||||
|
|
||||||
|
if (approverIds) {
|
||||||
|
input.approverIds = approverIds
|
||||||
|
.split(',')
|
||||||
|
.map(id => id.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await proboApiRequest.call(this, query, { input });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
json: responseData,
|
json: responseData,
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
||||||
import { proboApiRequest } from '../../GenericFunctions';
|
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Organization ID',
|
|
||||||
name: 'organizationId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['create'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the organization',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Name',
|
|
||||||
name: 'name',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['create'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The name of the snapshot',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Description',
|
|
||||||
name: 'description',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['create'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The description of the snapshot',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Type',
|
|
||||||
name: 'type',
|
|
||||||
type: 'options',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['create'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Assets',
|
|
||||||
value: 'ASSETS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Findings',
|
|
||||||
value: 'FINDINGS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Obligations',
|
|
||||||
value: 'OBLIGATIONS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Processing Activities',
|
|
||||||
value: 'PROCESSING_ACTIVITIES',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Risks',
|
|
||||||
value: 'RISKS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Statements of Applicability',
|
|
||||||
value: 'STATEMENTS_OF_APPLICABILITY',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Vendors',
|
|
||||||
value: 'VENDORS',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'RISKS',
|
|
||||||
description: 'The type of snapshot',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function execute(
|
|
||||||
this: IExecuteFunctions,
|
|
||||||
itemIndex: number,
|
|
||||||
): Promise<INodeExecutionData> {
|
|
||||||
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
|
|
||||||
const name = this.getNodeParameter('name', itemIndex) as string;
|
|
||||||
const description = this.getNodeParameter('description', itemIndex, '') as string;
|
|
||||||
const type = this.getNodeParameter('type', itemIndex) as string;
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
input: {
|
|
||||||
organizationId,
|
|
||||||
name,
|
|
||||||
...(description && { description }),
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const responseData = await proboApiRequest.call(this, query, variables);
|
|
||||||
|
|
||||||
return {
|
|
||||||
json: responseData,
|
|
||||||
pairedItem: { item: itemIndex },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
||||||
import { proboApiRequest } from '../../GenericFunctions';
|
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Snapshot ID',
|
|
||||||
name: 'snapshotId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['delete'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the snapshot to delete',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function execute(
|
|
||||||
this: IExecuteFunctions,
|
|
||||||
itemIndex: number,
|
|
||||||
): Promise<INodeExecutionData> {
|
|
||||||
const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string;
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
mutation DeleteSnapshot($input: DeleteSnapshotInput!) {
|
|
||||||
deleteSnapshot(input: $input) {
|
|
||||||
deletedSnapshotId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const responseData = await proboApiRequest.call(this, query, { input: { snapshotId } });
|
|
||||||
|
|
||||||
return {
|
|
||||||
json: responseData,
|
|
||||||
pairedItem: { item: itemIndex },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
||||||
import { proboApiRequest } from '../../GenericFunctions';
|
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Snapshot ID',
|
|
||||||
name: 'snapshotId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['get'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the snapshot',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function execute(
|
|
||||||
this: IExecuteFunctions,
|
|
||||||
itemIndex: number,
|
|
||||||
): Promise<INodeExecutionData> {
|
|
||||||
const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string;
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
query GetSnapshot($snapshotId: ID!) {
|
|
||||||
node(id: $snapshotId) {
|
|
||||||
... on Snapshot {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
snapshotId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const responseData = await proboApiRequest.call(this, query, variables);
|
|
||||||
|
|
||||||
return {
|
|
||||||
json: responseData,
|
|
||||||
pairedItem: { item: itemIndex },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow';
|
|
||||||
import { proboApiRequestAllItems } from '../../GenericFunctions';
|
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Organization ID',
|
|
||||||
name: 'organizationId',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['getAll'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The ID of the organization',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Return All',
|
|
||||||
name: 'returnAll',
|
|
||||||
type: 'boolean',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['getAll'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: false,
|
|
||||||
description: 'Whether to return all results or only up to a given limit',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Limit',
|
|
||||||
name: 'limit',
|
|
||||||
type: 'number',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
operation: ['getAll'],
|
|
||||||
returnAll: [false],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
typeOptions: {
|
|
||||||
minValue: 1,
|
|
||||||
},
|
|
||||||
default: 50,
|
|
||||||
description: 'Max number of results to return',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function execute(
|
|
||||||
this: IExecuteFunctions,
|
|
||||||
itemIndex: number,
|
|
||||||
): Promise<INodeExecutionData> {
|
|
||||||
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
|
|
||||||
const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean;
|
|
||||||
const limit = this.getNodeParameter('limit', itemIndex, 50) as number;
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
query GetSnapshots($organizationId: ID!, $first: Int, $after: CursorKey) {
|
|
||||||
node(id: $organizationId) {
|
|
||||||
... on Organization {
|
|
||||||
snapshots(first: $first, after: $after) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const snapshots = await proboApiRequestAllItems.call(
|
|
||||||
this,
|
|
||||||
query,
|
|
||||||
{ organizationId },
|
|
||||||
(response) => {
|
|
||||||
const data = response?.data as IDataObject | undefined;
|
|
||||||
const node = data?.node as IDataObject | undefined;
|
|
||||||
return node?.snapshots as IDataObject | undefined;
|
|
||||||
},
|
|
||||||
returnAll,
|
|
||||||
limit,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
json: { snapshots },
|
|
||||||
pairedItem: { item: itemIndex },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
import type { INodeProperties } from 'n8n-workflow';
|
|
||||||
import * as createOp from './create.operation';
|
|
||||||
import * as deleteOp from './delete.operation';
|
|
||||||
import * as getOp from './get.operation';
|
|
||||||
import * as getAllOp from './getAll.operation';
|
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
|
||||||
{
|
|
||||||
displayName: 'Operation',
|
|
||||||
name: 'operation',
|
|
||||||
type: 'options',
|
|
||||||
noDataExpression: true,
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
resource: ['snapshot'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Create',
|
|
||||||
value: 'create',
|
|
||||||
description: 'Create a new snapshot',
|
|
||||||
action: 'Create a snapshot',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Delete',
|
|
||||||
value: 'delete',
|
|
||||||
description: 'Delete a snapshot',
|
|
||||||
action: 'Delete a snapshot',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Get',
|
|
||||||
value: 'get',
|
|
||||||
description: 'Get a snapshot',
|
|
||||||
action: 'Get a snapshot',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Get Many',
|
|
||||||
value: 'getAll',
|
|
||||||
description: 'Get many snapshots',
|
|
||||||
action: 'Get many snapshots',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'create',
|
|
||||||
},
|
|
||||||
...createOp.description,
|
|
||||||
...deleteOp.description,
|
|
||||||
...getOp.description,
|
|
||||||
...getAllOp.description,
|
|
||||||
];
|
|
||||||
|
|
||||||
export { createOp as create, deleteOp as delete, getOp as get, getAllOp as getAll };
|
|
||||||
148
pkg/cmd/risk/publish/publish.go
Normal file
148
pkg/cmd/risk/publish/publish.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package publish
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.probo.inc/probo/pkg/cli/api"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const publishMutation = `
|
||||||
|
mutation($input: PublishRiskListInput!) {
|
||||||
|
publishRiskList(input: $input) {
|
||||||
|
documentEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
documentVersionEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
major
|
||||||
|
minor
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type publishResponse struct {
|
||||||
|
PublishRiskList struct {
|
||||||
|
DocumentEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentEdge"`
|
||||||
|
DocumentVersionEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Major int `json:"major"`
|
||||||
|
Minor int `json:"minor"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"documentVersionEdge"`
|
||||||
|
} `json:"publishRiskList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmdPublish(f *cmdutil.Factory) *cobra.Command {
|
||||||
|
var (
|
||||||
|
flagOrg string
|
||||||
|
flagApprover []string
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "publish",
|
||||||
|
Short: "Publish the risk register as a document version",
|
||||||
|
Example: ` # Publish the risk register
|
||||||
|
prb risk publish --org ORG_ID
|
||||||
|
|
||||||
|
# Publish with approvers
|
||||||
|
prb risk publish --org ORG_ID --approver PROFILE_ID1 --approver PROFILE_ID2`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cfg, err := f.Config()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
host, hc, err := cfg.DefaultHost()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagOrg == "" {
|
||||||
|
flagOrg = hc.Organization
|
||||||
|
}
|
||||||
|
if flagOrg == "" {
|
||||||
|
return fmt.Errorf("organization is required: pass --org or run `prb auth login`")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := api.NewClient(
|
||||||
|
host,
|
||||||
|
hc.Token,
|
||||||
|
"/api/console/v1/graphql",
|
||||||
|
cfg.HTTPTimeoutDuration(),
|
||||||
|
cmdutil.TokenRefreshOption(cfg, host, hc),
|
||||||
|
)
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
"organizationId": flagOrg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(flagApprover) > 0 {
|
||||||
|
input["approverIds"] = flagApprover
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := client.Do(
|
||||||
|
publishMutation,
|
||||||
|
map[string]any{"input": input},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp publishResponse
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return fmt.Errorf("cannot parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := resp.PublishRiskList.DocumentVersionEdge.Node
|
||||||
|
_, _ = fmt.Fprintf(
|
||||||
|
f.IOStreams.Out,
|
||||||
|
"Published risk register %s (v%d.%d)\n",
|
||||||
|
v.Title,
|
||||||
|
v.Major,
|
||||||
|
v.Minor,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
||||||
|
cmd.Flags().StringArrayVar(&flagApprover, "approver", nil, "Approver profile ID (can be repeated)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/cmd/risk/create"
|
"go.probo.inc/probo/pkg/cmd/risk/create"
|
||||||
"go.probo.inc/probo/pkg/cmd/risk/delete"
|
"go.probo.inc/probo/pkg/cmd/risk/delete"
|
||||||
"go.probo.inc/probo/pkg/cmd/risk/list"
|
"go.probo.inc/probo/pkg/cmd/risk/list"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/risk/publish"
|
||||||
"go.probo.inc/probo/pkg/cmd/risk/update"
|
"go.probo.inc/probo/pkg/cmd/risk/update"
|
||||||
"go.probo.inc/probo/pkg/cmd/risk/view"
|
"go.probo.inc/probo/pkg/cmd/risk/view"
|
||||||
)
|
)
|
||||||
@@ -35,6 +36,7 @@ func NewCmdRisk(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmd.AddCommand(view.NewCmdView(f))
|
cmd.AddCommand(view.NewCmdView(f))
|
||||||
cmd.AddCommand(update.NewCmdUpdate(f))
|
cmd.AddCommand(update.NewCmdUpdate(f))
|
||||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
cmd.AddCommand(delete.NewCmdDelete(f))
|
||||||
|
cmd.AddCommand(publish.NewCmdPublish(f))
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import (
|
|||||||
processingactivity "go.probo.inc/probo/pkg/cmd/processing-activity"
|
processingactivity "go.probo.inc/probo/pkg/cmd/processing-activity"
|
||||||
rightsrequest "go.probo.inc/probo/pkg/cmd/rights-request"
|
rightsrequest "go.probo.inc/probo/pkg/cmd/rights-request"
|
||||||
"go.probo.inc/probo/pkg/cmd/risk"
|
"go.probo.inc/probo/pkg/cmd/risk"
|
||||||
"go.probo.inc/probo/pkg/cmd/snapshot"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/soa"
|
"go.probo.inc/probo/pkg/cmd/soa"
|
||||||
"go.probo.inc/probo/pkg/cmd/task"
|
"go.probo.inc/probo/pkg/cmd/task"
|
||||||
"go.probo.inc/probo/pkg/cmd/tia"
|
"go.probo.inc/probo/pkg/cmd/tia"
|
||||||
@@ -112,7 +111,6 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmd.AddCommand(processingactivity.NewCmdProcessingActivity(f))
|
cmd.AddCommand(processingactivity.NewCmdProcessingActivity(f))
|
||||||
cmd.AddCommand(rightsrequest.NewCmdRightsRequest(f))
|
cmd.AddCommand(rightsrequest.NewCmdRightsRequest(f))
|
||||||
cmd.AddCommand(risk.NewCmdRisk(f))
|
cmd.AddCommand(risk.NewCmdRisk(f))
|
||||||
cmd.AddCommand(snapshot.NewCmdSnapshot(f))
|
|
||||||
cmd.AddCommand(soa.NewCmdSoa(f))
|
cmd.AddCommand(soa.NewCmdSoa(f))
|
||||||
cmd.AddCommand(task.NewCmdTask(f))
|
cmd.AddCommand(task.NewCmdTask(f))
|
||||||
cmd.AddCommand(tia.NewCmdTIA(f))
|
cmd.AddCommand(tia.NewCmdTIA(f))
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package create
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.probo.inc/probo/pkg/cli/api"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createMutation = `
|
|
||||||
mutation($input: CreateSnapshotInput!) {
|
|
||||||
createSnapshot(input: $input) {
|
|
||||||
snapshotEdge {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
type createResponse struct {
|
|
||||||
CreateSnapshot struct {
|
|
||||||
SnapshotEdge struct {
|
|
||||||
Node struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
} `json:"node"`
|
|
||||||
} `json:"snapshotEdge"`
|
|
||||||
} `json:"createSnapshot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
|
||||||
var (
|
|
||||||
flagOrg string
|
|
||||||
flagName string
|
|
||||||
flagType string
|
|
||||||
flagDescription string
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "create",
|
|
||||||
Short: "Create a new snapshot",
|
|
||||||
Example: ` # Create a snapshot interactively
|
|
||||||
prb snapshot create
|
|
||||||
|
|
||||||
# Create a snapshot non-interactively
|
|
||||||
prb snapshot create --name "Q1 2026 Risks" --type RISKS`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
cfg, err := f.Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
host, hc, err := cfg.DefaultHost()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := api.NewClient(
|
|
||||||
host,
|
|
||||||
hc.Token,
|
|
||||||
"/api/console/v1/graphql",
|
|
||||||
cfg.HTTPTimeoutDuration(),
|
|
||||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
|
||||||
)
|
|
||||||
|
|
||||||
if flagOrg == "" {
|
|
||||||
flagOrg = hc.Organization
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagOrg == "" {
|
|
||||||
return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.IOStreams.IsInteractive() {
|
|
||||||
if flagName == "" {
|
|
||||||
err := huh.NewInput().
|
|
||||||
Title("Snapshot name").
|
|
||||||
Value(&flagName).
|
|
||||||
Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagType == "" {
|
|
||||||
err := huh.NewSelect[string]().
|
|
||||||
Title("Snapshot type").
|
|
||||||
Options(
|
|
||||||
huh.NewOption("Risks", "RISKS"),
|
|
||||||
huh.NewOption("Vendors", "VENDORS"),
|
|
||||||
huh.NewOption("Assets", "ASSETS"),
|
|
||||||
huh.NewOption("Findings", "FINDINGS"),
|
|
||||||
huh.NewOption("Obligations", "OBLIGATIONS"),
|
|
||||||
huh.NewOption("Processing Activities", "PROCESSING_ACTIVITIES"),
|
|
||||||
huh.NewOption("Statements of Applicability", "STATEMENTS_OF_APPLICABILITY"),
|
|
||||||
).
|
|
||||||
Value(&flagType).
|
|
||||||
Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagName == "" {
|
|
||||||
return fmt.Errorf("name is required; pass --name or run interactively")
|
|
||||||
}
|
|
||||||
if flagType == "" {
|
|
||||||
return fmt.Errorf("type is required; pass --type or run interactively")
|
|
||||||
}
|
|
||||||
|
|
||||||
input := map[string]any{
|
|
||||||
"organizationId": flagOrg,
|
|
||||||
"name": flagName,
|
|
||||||
"type": flagType,
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagDescription != "" {
|
|
||||||
input["description"] = flagDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := client.Do(
|
|
||||||
createMutation,
|
|
||||||
map[string]any{"input": input},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp createResponse
|
|
||||||
if err := json.Unmarshal(data, &resp); err != nil {
|
|
||||||
return fmt.Errorf("cannot parse response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := resp.CreateSnapshot.SnapshotEdge.Node
|
|
||||||
_, _ = fmt.Fprintf(
|
|
||||||
f.IOStreams.Out,
|
|
||||||
"Created snapshot %s (%s)\n",
|
|
||||||
s.ID,
|
|
||||||
s.Name,
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
|
||||||
cmd.Flags().StringVar(&flagName, "name", "", "Snapshot name (required)")
|
|
||||||
cmd.Flags().StringVar(&flagType, "type", "", "Snapshot type: RISKS, VENDORS, ASSETS, FINDINGS, OBLIGATIONS, PROCESSING_ACTIVITIES, STATEMENTS_OF_APPLICABILITY (required)")
|
|
||||||
cmd.Flags().StringVar(&flagDescription, "description", "", "Snapshot description")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package delete
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.probo.inc/probo/pkg/cli/api"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteMutation = `
|
|
||||||
mutation($input: DeleteSnapshotInput!) {
|
|
||||||
deleteSnapshot(input: $input) {
|
|
||||||
deletedSnapshotId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
|
|
||||||
var flagYes bool
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "delete <id>",
|
|
||||||
Short: "Delete a snapshot",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if !flagYes {
|
|
||||||
if !f.IOStreams.IsInteractive() {
|
|
||||||
return fmt.Errorf("cannot delete snapshot: confirmation required, use --yes to confirm")
|
|
||||||
}
|
|
||||||
|
|
||||||
var confirmed bool
|
|
||||||
err := huh.NewConfirm().
|
|
||||||
Title(fmt.Sprintf("Delete snapshot %s?", args[0])).
|
|
||||||
Value(&confirmed).
|
|
||||||
Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !confirmed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := f.Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
host, hc, err := cfg.DefaultHost()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := api.NewClient(
|
|
||||||
host,
|
|
||||||
hc.Token,
|
|
||||||
"/api/console/v1/graphql",
|
|
||||||
cfg.HTTPTimeoutDuration(),
|
|
||||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err = client.Do(
|
|
||||||
deleteMutation,
|
|
||||||
map[string]any{
|
|
||||||
"input": map[string]any{
|
|
||||||
"snapshotId": args[0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(
|
|
||||||
f.IOStreams.Out,
|
|
||||||
"Deleted snapshot %s\n",
|
|
||||||
args[0],
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package list
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.probo.inc/probo/pkg/cli/api"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const listQuery = `
|
|
||||||
query($id: ID!, $first: Int, $after: CursorKey, $orderBy: SnapshotOrder) {
|
|
||||||
node(id: $id) {
|
|
||||||
__typename
|
|
||||||
... on Organization {
|
|
||||||
snapshots(first: $first, after: $after, orderBy: $orderBy) {
|
|
||||||
totalCount
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
type snapshot struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
|
||||||
var (
|
|
||||||
flagOrg string
|
|
||||||
flagLimit int
|
|
||||||
flagOrderBy string
|
|
||||||
flagOrderDir string
|
|
||||||
flagOutput *string
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Short: "List snapshots in an organization",
|
|
||||||
Aliases: []string{"ls"},
|
|
||||||
Example: ` # List snapshots in the default organization
|
|
||||||
prb snapshot list
|
|
||||||
|
|
||||||
# List snapshots sorted by name
|
|
||||||
prb snapshot ls --order-by NAME --json`,
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := f.Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
host, hc, err := cfg.DefaultHost()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := api.NewClient(
|
|
||||||
host,
|
|
||||||
hc.Token,
|
|
||||||
"/api/console/v1/graphql",
|
|
||||||
cfg.HTTPTimeoutDuration(),
|
|
||||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
|
||||||
)
|
|
||||||
|
|
||||||
if flagOrg == "" {
|
|
||||||
flagOrg = hc.Organization
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagOrg == "" {
|
|
||||||
return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'")
|
|
||||||
}
|
|
||||||
|
|
||||||
variables := map[string]any{
|
|
||||||
"id": flagOrg,
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagOrderBy != "" {
|
|
||||||
if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME", "TYPE"}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
variables["orderBy"] = map[string]any{
|
|
||||||
"field": flagOrderBy,
|
|
||||||
"direction": flagOrderDir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshots, totalCount, err := api.Paginate(
|
|
||||||
client,
|
|
||||||
listQuery,
|
|
||||||
variables,
|
|
||||||
flagLimit,
|
|
||||||
func(data json.RawMessage) (*api.Connection[snapshot], error) {
|
|
||||||
var resp struct {
|
|
||||||
Node *struct {
|
|
||||||
Typename string `json:"__typename"`
|
|
||||||
Snapshots api.Connection[snapshot] `json:"snapshots"`
|
|
||||||
} `json:"node"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &resp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.Node == nil {
|
|
||||||
return nil, fmt.Errorf("organization %s not found", flagOrg)
|
|
||||||
}
|
|
||||||
if resp.Node.Typename != "Organization" {
|
|
||||||
return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename)
|
|
||||||
}
|
|
||||||
return &resp.Node.Snapshots, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flagOutput == cmdutil.OutputJSON {
|
|
||||||
return cmdutil.PrintJSON(f.IOStreams.Out, snapshots)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(snapshots) == 0 {
|
|
||||||
_, _ = fmt.Fprintln(f.IOStreams.Out, "No snapshots found.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([][]string, 0, len(snapshots))
|
|
||||||
for _, s := range snapshots {
|
|
||||||
rows = append(rows, []string{
|
|
||||||
s.ID,
|
|
||||||
s.Name,
|
|
||||||
s.Type,
|
|
||||||
cmdutil.FormatTime(s.CreatedAt),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t := cmdutil.NewTable("ID", "NAME", "TYPE", "CREATED AT").Rows(rows...)
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
|
|
||||||
|
|
||||||
if totalCount > len(snapshots) {
|
|
||||||
_, _ = fmt.Fprintf(
|
|
||||||
f.IOStreams.ErrOut,
|
|
||||||
"\nShowing %d of %d snapshots\n",
|
|
||||||
len(snapshots),
|
|
||||||
totalCount,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
|
||||||
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of snapshots to list")
|
|
||||||
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME, TYPE)")
|
|
||||||
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
|
|
||||||
flagOutput = cmdutil.AddOutputFlag(cmd)
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/snapshot/create"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/snapshot/delete"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/snapshot/list"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/snapshot/view"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewCmdSnapshot(f *cmdutil.Factory) *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "snapshot <command>",
|
|
||||||
Short: "Manage snapshots",
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.AddCommand(list.NewCmdList(f))
|
|
||||||
cmd.AddCommand(create.NewCmdCreate(f))
|
|
||||||
cmd.AddCommand(view.NewCmdView(f))
|
|
||||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package view
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.probo.inc/probo/pkg/cli/api"
|
|
||||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const viewQuery = `
|
|
||||||
query($id: ID!) {
|
|
||||||
node(id: $id) {
|
|
||||||
__typename
|
|
||||||
... on Snapshot {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
type
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
type viewResponse struct {
|
|
||||||
Node *struct {
|
|
||||||
Typename string `json:"__typename"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description *string `json:"description"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
} `json:"node"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCmdView(f *cmdutil.Factory) *cobra.Command {
|
|
||||||
var flagOutput *string
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "view <id>",
|
|
||||||
Short: "View a snapshot",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := f.Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
host, hc, err := cfg.DefaultHost()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := api.NewClient(
|
|
||||||
host,
|
|
||||||
hc.Token,
|
|
||||||
"/api/console/v1/graphql",
|
|
||||||
cfg.HTTPTimeoutDuration(),
|
|
||||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
|
||||||
)
|
|
||||||
|
|
||||||
data, err := client.Do(
|
|
||||||
viewQuery,
|
|
||||||
map[string]any{"id": args[0]},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp viewResponse
|
|
||||||
if err := json.Unmarshal(data, &resp); err != nil {
|
|
||||||
return fmt.Errorf("cannot parse response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Node == nil {
|
|
||||||
return fmt.Errorf("snapshot %s not found", args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Node.Typename != "Snapshot" {
|
|
||||||
return fmt.Errorf("expected Snapshot node, got %s", resp.Node.Typename)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flagOutput == cmdutil.OutputJSON {
|
|
||||||
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := resp.Node
|
|
||||||
out := f.IOStreams.Out
|
|
||||||
|
|
||||||
bold := lipgloss.NewStyle().Bold(true)
|
|
||||||
label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22)
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(s.Name))
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), s.ID)
|
|
||||||
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), s.Type)
|
|
||||||
|
|
||||||
if s.Description != nil && *s.Description != "" {
|
|
||||||
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *s.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(out)
|
|
||||||
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(s.CreatedAt))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
flagOutput = cmdutil.AddOutputFlag(cmd)
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -73,8 +73,6 @@ func ResourceTypeName(entityType uint16) string {
|
|||||||
return "Obligation"
|
return "Obligation"
|
||||||
case VendorServiceEntityType:
|
case VendorServiceEntityType:
|
||||||
return "VendorService"
|
return "VendorService"
|
||||||
case SnapshotEntityType:
|
|
||||||
return "Snapshot"
|
|
||||||
case ProcessingActivityEntityType:
|
case ProcessingActivityEntityType:
|
||||||
return "ProcessingActivity"
|
return "ProcessingActivity"
|
||||||
case TrustCenterReferenceEntityType:
|
case TrustCenterReferenceEntityType:
|
||||||
|
|||||||
@@ -970,77 +970,6 @@ WHERE %s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controls) LoadBySnapshotID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Querier,
|
|
||||||
scope Scoper,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
cursor *page.Cursor[ControlOrderField],
|
|
||||||
filter *ControlFilter,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
WITH ctrl AS (
|
|
||||||
SELECT
|
|
||||||
c.id,
|
|
||||||
c.section_title,
|
|
||||||
c.framework_id,
|
|
||||||
c.organization_id,
|
|
||||||
c.tenant_id,
|
|
||||||
c.name,
|
|
||||||
c.description,
|
|
||||||
c.best_practice,
|
|
||||||
c.not_implemented_justification,
|
|
||||||
c.maturity_level,
|
|
||||||
c.created_at,
|
|
||||||
c.updated_at,
|
|
||||||
c.search_vector
|
|
||||||
FROM
|
|
||||||
controls c
|
|
||||||
INNER JOIN
|
|
||||||
controls_snapshots cs ON c.id = cs.control_id
|
|
||||||
WHERE
|
|
||||||
cs.snapshot_id = @snapshot_id
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
section_title,
|
|
||||||
framework_id,
|
|
||||||
organization_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
best_practice,
|
|
||||||
not_implemented_justification,
|
|
||||||
maturity_level,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM
|
|
||||||
ctrl
|
|
||||||
WHERE %s
|
|
||||||
AND %s
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.NamedArgs{"snapshot_id": snapshotID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, filter.SQLArguments())
|
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query controls: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect controls: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = controls
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controls) CountByStatementOfApplicabilityID(
|
func (c *Controls) CountByStatementOfApplicabilityID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
ControlSnapshot struct {
|
|
||||||
ControlID gid.GID `db:"control_id"`
|
|
||||||
SnapshotID gid.GID `db:"snapshot_id"`
|
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlSnapshots []*ControlSnapshot
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cs ControlSnapshot) Upsert(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Querier,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
INSERT INTO
|
|
||||||
controls_snapshots (
|
|
||||||
control_id,
|
|
||||||
snapshot_id,
|
|
||||||
organization_id,
|
|
||||||
tenant_id,
|
|
||||||
created_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
@control_id,
|
|
||||||
@snapshot_id,
|
|
||||||
@organization_id,
|
|
||||||
@tenant_id,
|
|
||||||
@created_at
|
|
||||||
)
|
|
||||||
ON CONFLICT (control_id, snapshot_id) DO NOTHING;
|
|
||||||
`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"control_id": cs.ControlID,
|
|
||||||
"snapshot_id": cs.SnapshotID,
|
|
||||||
"organization_id": cs.OrganizationID,
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"created_at": cs.CreatedAt,
|
|
||||||
}
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs ControlSnapshot) Delete(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
controlID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
DELETE
|
|
||||||
FROM
|
|
||||||
controls_snapshots
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND control_id = @control_id
|
|
||||||
AND snapshot_id = @snapshot_id;
|
|
||||||
`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"control_id": controlID,
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -54,7 +54,7 @@ const (
|
|||||||
_ uint16 = 28 // NonconformityEntityType - removed
|
_ uint16 = 28 // NonconformityEntityType - removed
|
||||||
ObligationEntityType uint16 = 29
|
ObligationEntityType uint16 = 29
|
||||||
VendorServiceEntityType uint16 = 30
|
VendorServiceEntityType uint16 = 30
|
||||||
SnapshotEntityType uint16 = 31
|
_ uint16 = 31 // SnapshotEntityType - removed
|
||||||
_ uint16 = 32 // ContinualImprovementEntityType - removed
|
_ uint16 = 32 // ContinualImprovementEntityType - removed
|
||||||
ProcessingActivityEntityType uint16 = 33
|
ProcessingActivityEntityType uint16 = 33
|
||||||
ExportJobEntityType uint16 = 34
|
ExportJobEntityType uint16 = 34
|
||||||
@@ -176,8 +176,6 @@ func NewEntityFromID(id gid.GID) (any, bool) {
|
|||||||
return &Obligation{ID: id}, true
|
return &Obligation{ID: id}, true
|
||||||
case VendorServiceEntityType:
|
case VendorServiceEntityType:
|
||||||
return &VendorService{ID: id}, true
|
return &VendorService{ID: id}, true
|
||||||
case SnapshotEntityType:
|
|
||||||
return &Snapshot{ID: id}, true
|
|
||||||
case ProcessingActivityEntityType:
|
case ProcessingActivityEntityType:
|
||||||
return &ProcessingActivity{ID: id}, true
|
return &ProcessingActivity{ID: id}, true
|
||||||
case ExportJobEntityType:
|
case ExportJobEntityType:
|
||||||
|
|||||||
90
pkg/coredata/migrations/20260429T150423Z.sql
Normal file
90
pkg/coredata/migrations/20260429T150423Z.sql
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
--
|
||||||
|
-- Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
-- purpose with or without fee is hereby granted, provided that the above
|
||||||
|
-- copyright notice and this permission notice appear in all copies.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
-- PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
ALTER TABLE generated_documents
|
||||||
|
ADD COLUMN risks_document_id TEXT REFERENCES documents(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Backfill controls_documents from the legacy controls_snapshots links.
|
||||||
|
-- For every snapshot type whose register is now an org-level generated
|
||||||
|
-- document, link each control that was attached to a snapshot to the
|
||||||
|
-- corresponding generated document. Best effort: skips rows whose target
|
||||||
|
-- document hasn't been created yet (the matching `cmd/migrate-*` data
|
||||||
|
-- migration must have already run). ON CONFLICT keeps the migration
|
||||||
|
-- idempotent and tolerant of pre-existing mappings.
|
||||||
|
INSERT INTO controls_documents (control_id, document_id, organization_id, tenant_id, created_at)
|
||||||
|
SELECT DISTINCT
|
||||||
|
cs.control_id,
|
||||||
|
CASE s.type
|
||||||
|
WHEN 'RISKS' THEN gd.risks_document_id
|
||||||
|
WHEN 'VENDORS' THEN gd.vendors_document_id
|
||||||
|
WHEN 'ASSETS' THEN gd.asset_list_document_id
|
||||||
|
WHEN 'DATA' THEN gd.data_document_id
|
||||||
|
WHEN 'FINDINGS' THEN gd.findings_document_id
|
||||||
|
WHEN 'OBLIGATIONS' THEN gd.obligations_document_id
|
||||||
|
WHEN 'PROCESSING_ACTIVITIES' THEN gd.processing_activities_document_id
|
||||||
|
END AS document_id,
|
||||||
|
s.organization_id,
|
||||||
|
s.tenant_id,
|
||||||
|
NOW()
|
||||||
|
FROM controls_snapshots cs
|
||||||
|
INNER JOIN snapshots s ON s.id = cs.snapshot_id
|
||||||
|
LEFT JOIN generated_documents gd ON gd.organization_id = s.organization_id
|
||||||
|
WHERE s.type IN (
|
||||||
|
'RISKS',
|
||||||
|
'VENDORS',
|
||||||
|
'ASSETS',
|
||||||
|
'DATA',
|
||||||
|
'FINDINGS',
|
||||||
|
'OBLIGATIONS',
|
||||||
|
'PROCESSING_ACTIVITIES'
|
||||||
|
)
|
||||||
|
AND CASE s.type
|
||||||
|
WHEN 'RISKS' THEN gd.risks_document_id
|
||||||
|
WHEN 'VENDORS' THEN gd.vendors_document_id
|
||||||
|
WHEN 'ASSETS' THEN gd.asset_list_document_id
|
||||||
|
WHEN 'DATA' THEN gd.data_document_id
|
||||||
|
WHEN 'FINDINGS' THEN gd.findings_document_id
|
||||||
|
WHEN 'OBLIGATIONS' THEN gd.obligations_document_id
|
||||||
|
WHEN 'PROCESSING_ACTIVITIES' THEN gd.processing_activities_document_id
|
||||||
|
END IS NOT NULL
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- For STATEMENTS_OF_APPLICABILITY snapshots, the published document lives on
|
||||||
|
-- the source SOA (the live row, snapshot_id IS NULL). Link controls that
|
||||||
|
-- were attached to a SOA snapshot to that source SOA's document.
|
||||||
|
INSERT INTO controls_documents (control_id, document_id, organization_id, tenant_id, created_at)
|
||||||
|
SELECT DISTINCT
|
||||||
|
cs.control_id,
|
||||||
|
live_soa.document_id,
|
||||||
|
s.organization_id,
|
||||||
|
s.tenant_id,
|
||||||
|
NOW()
|
||||||
|
FROM controls_snapshots cs
|
||||||
|
INNER JOIN snapshots s ON s.id = cs.snapshot_id
|
||||||
|
INNER JOIN statements_of_applicability snap_soa ON snap_soa.snapshot_id = s.id
|
||||||
|
INNER JOIN statements_of_applicability live_soa
|
||||||
|
ON live_soa.id = snap_soa.source_id
|
||||||
|
AND live_soa.snapshot_id IS NULL
|
||||||
|
WHERE s.type = 'STATEMENTS_OF_APPLICABILITY'
|
||||||
|
AND live_soa.document_id IS NOT NULL
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Drop the trailing " List" suffix from previously published register
|
||||||
|
-- documents so the version title matches the new naming convention used by
|
||||||
|
-- the publish flow. Restricted to REGISTER document types so unrelated
|
||||||
|
-- documents that happen to share a title aren't touched.
|
||||||
|
UPDATE document_versions SET title = 'Assets' WHERE title = 'Asset List' AND document_type = 'REGISTER';
|
||||||
|
UPDATE document_versions SET title = 'Data' WHERE title = 'Data List' AND document_type = 'REGISTER';
|
||||||
|
UPDATE document_versions SET title = 'Findings' WHERE title = 'Finding List' AND document_type = 'REGISTER';
|
||||||
|
UPDATE document_versions SET title = 'Obligations' WHERE title = 'Obligation List' AND document_type = 'REGISTER';
|
||||||
@@ -27,6 +27,113 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (r Risk) GetGeneratedDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) (*gid.GID, error) {
|
||||||
|
var documentID *gid.GID
|
||||||
|
|
||||||
|
err := conn.QueryRow(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
risks_document_id
|
||||||
|
FROM
|
||||||
|
generated_documents
|
||||||
|
WHERE
|
||||||
|
organization_id = @organization_id
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{"organization_id": organizationID},
|
||||||
|
).Scan(&documentID)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get risk list document ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return documentID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Risk) UpsertGeneratedDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Tx,
|
||||||
|
organizationID gid.GID,
|
||||||
|
tenantID gid.TenantID,
|
||||||
|
documentID gid.GID,
|
||||||
|
) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
_, err := conn.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO generated_documents (
|
||||||
|
organization_id,
|
||||||
|
tenant_id,
|
||||||
|
risks_document_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
@organization_id,
|
||||||
|
@tenant_id,
|
||||||
|
@risks_document_id,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
|
)
|
||||||
|
ON CONFLICT (organization_id) DO UPDATE
|
||||||
|
SET
|
||||||
|
risks_document_id = @risks_document_id,
|
||||||
|
updated_at = @updated_at
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"organization_id": organizationID,
|
||||||
|
"tenant_id": tenantID,
|
||||||
|
"risks_document_id": documentID,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot upsert risk list document ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Risk) ClearGeneratedDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Tx,
|
||||||
|
documentIDs []gid.GID,
|
||||||
|
) error {
|
||||||
|
ids := make([]string, len(documentIDs))
|
||||||
|
for i, id := range documentIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
UPDATE
|
||||||
|
generated_documents
|
||||||
|
SET
|
||||||
|
risks_document_id = NULL,
|
||||||
|
updated_at = @now
|
||||||
|
WHERE
|
||||||
|
risks_document_id = ANY(@ids)
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"ids": ids,
|
||||||
|
"now": time.Now(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot clear risk list document references: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Risk struct {
|
Risk struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
@@ -43,8 +150,6 @@ type (
|
|||||||
ResidualLikelihood int `db:"residual_likelihood"`
|
ResidualLikelihood int `db:"residual_likelihood"`
|
||||||
ResidualImpact int `db:"residual_impact"`
|
ResidualImpact int `db:"residual_impact"`
|
||||||
ResidualRiskScore int `db:"residual_risk_score"`
|
ResidualRiskScore int `db:"residual_risk_score"`
|
||||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
|
||||||
SourceID *gid.GID `db:"source_id"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
|
||||||
@@ -53,10 +158,6 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Risks []*Risk
|
Risks []*Risk
|
||||||
|
|
||||||
RiskSnapshotter interface {
|
|
||||||
InsertRiskSnapshots(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey {
|
func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey {
|
||||||
@@ -106,14 +207,14 @@ WITH rsks AS (
|
|||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
r.tenant_id,
|
r.tenant_id,
|
||||||
r.search_vector,
|
r.search_vector
|
||||||
r.snapshot_id
|
|
||||||
FROM
|
FROM
|
||||||
risks r
|
risks r
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
risks_measures rm ON r.id = rm.risk_id
|
risks_measures rm ON r.id = rm.risk_id
|
||||||
WHERE
|
WHERE
|
||||||
rm.measure_id = @measure_id
|
rm.measure_id = @measure_id
|
||||||
|
AND r.snapshot_id IS NULL
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(id)
|
COUNT(id)
|
||||||
@@ -165,8 +266,6 @@ WITH rsks AS (
|
|||||||
r.residual_likelihood,
|
r.residual_likelihood,
|
||||||
r.residual_impact,
|
r.residual_impact,
|
||||||
r.residual_risk_score,
|
r.residual_risk_score,
|
||||||
r.snapshot_id,
|
|
||||||
r.source_id,
|
|
||||||
r.search_vector,
|
r.search_vector,
|
||||||
r.created_at,
|
r.created_at,
|
||||||
r.updated_at
|
r.updated_at
|
||||||
@@ -178,6 +277,7 @@ WITH rsks AS (
|
|||||||
iam_membership_profiles p ON r.owner_profile_id = p.id
|
iam_membership_profiles p ON r.owner_profile_id = p.id
|
||||||
WHERE
|
WHERE
|
||||||
rm.measure_id = @measure_id
|
rm.measure_id = @measure_id
|
||||||
|
AND r.snapshot_id IS NULL
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@@ -195,8 +295,6 @@ SELECT
|
|||||||
residual_likelihood,
|
residual_likelihood,
|
||||||
residual_impact,
|
residual_impact,
|
||||||
residual_risk_score,
|
residual_risk_score,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -240,6 +338,7 @@ SELECT
|
|||||||
FROM risks
|
FROM risks
|
||||||
WHERE %s
|
WHERE %s
|
||||||
AND organization_id = @organization_id
|
AND organization_id = @organization_id
|
||||||
|
AND snapshot_id IS NULL
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
@@ -285,8 +384,6 @@ WITH rsks AS (
|
|||||||
r.residual_impact,
|
r.residual_impact,
|
||||||
r.residual_risk_score,
|
r.residual_risk_score,
|
||||||
r.category,
|
r.category,
|
||||||
r.snapshot_id,
|
|
||||||
r.source_id,
|
|
||||||
r.search_vector,
|
r.search_vector,
|
||||||
r.created_at,
|
r.created_at,
|
||||||
r.updated_at
|
r.updated_at
|
||||||
@@ -296,6 +393,7 @@ WITH rsks AS (
|
|||||||
iam_membership_profiles p ON r.owner_profile_id = p.id
|
iam_membership_profiles p ON r.owner_profile_id = p.id
|
||||||
WHERE
|
WHERE
|
||||||
r.organization_id = @organization_id
|
r.organization_id = @organization_id
|
||||||
|
AND r.snapshot_id IS NULL
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@@ -313,8 +411,6 @@ SELECT
|
|||||||
residual_impact,
|
residual_impact,
|
||||||
residual_risk_score,
|
residual_risk_score,
|
||||||
category,
|
category,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -345,6 +441,58 @@ WHERE %s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Risks) LoadAllByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.organization_id,
|
||||||
|
r.name,
|
||||||
|
r.description,
|
||||||
|
r.category,
|
||||||
|
r.owner_profile_id,
|
||||||
|
NULL as owner_full_name,
|
||||||
|
r.treatment,
|
||||||
|
r.note,
|
||||||
|
r.inherent_likelihood,
|
||||||
|
r.inherent_impact,
|
||||||
|
r.inherent_risk_score,
|
||||||
|
r.residual_likelihood,
|
||||||
|
r.residual_impact,
|
||||||
|
r.residual_risk_score,
|
||||||
|
r.created_at,
|
||||||
|
r.updated_at
|
||||||
|
FROM
|
||||||
|
risks r
|
||||||
|
WHERE %s
|
||||||
|
AND r.organization_id = @organization_id
|
||||||
|
AND r.snapshot_id IS NULL
|
||||||
|
ORDER BY r.name ASC, r.id ASC
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query risks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
risks, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Risk])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect risks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*r = risks
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Risk) LoadByID(
|
func (r *Risk) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
@@ -368,8 +516,6 @@ SELECT
|
|||||||
residual_likelihood,
|
residual_likelihood,
|
||||||
residual_impact,
|
residual_impact,
|
||||||
residual_risk_score,
|
residual_risk_score,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM risks
|
FROM risks
|
||||||
@@ -424,8 +570,6 @@ SELECT
|
|||||||
residual_likelihood,
|
residual_likelihood,
|
||||||
residual_impact,
|
residual_impact,
|
||||||
residual_risk_score,
|
residual_risk_score,
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM risks
|
FROM risks
|
||||||
@@ -567,14 +711,14 @@ WITH rsks AS (
|
|||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
r.tenant_id,
|
r.tenant_id,
|
||||||
r.search_vector,
|
r.search_vector
|
||||||
r.snapshot_id
|
|
||||||
FROM
|
FROM
|
||||||
risks r
|
risks r
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
risks_documents rd ON r.id = rd.risk_id
|
risks_documents rd ON r.id = rd.risk_id
|
||||||
WHERE
|
WHERE
|
||||||
rd.document_id = @document_id
|
rd.document_id = @document_id
|
||||||
|
AND r.snapshot_id IS NULL
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(id)
|
COUNT(id)
|
||||||
@@ -598,78 +742,3 @@ WHERE %s
|
|||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Risks) Snapshot(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error {
|
|
||||||
if err := r.InsertRiskSnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil {
|
|
||||||
return fmt.Errorf("cannot create risk snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Risks) InsertRiskSnapshots(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
query := `
|
|
||||||
INSERT INTO risks (
|
|
||||||
tenant_id,
|
|
||||||
id,
|
|
||||||
snapshot_id,
|
|
||||||
source_id,
|
|
||||||
organization_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
category,
|
|
||||||
treatment,
|
|
||||||
note,
|
|
||||||
owner_profile_id,
|
|
||||||
inherent_likelihood,
|
|
||||||
inherent_impact,
|
|
||||||
residual_likelihood,
|
|
||||||
residual_impact,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
@tenant_id,
|
|
||||||
generate_gid(decode_base64_unpadded(@tenant_id), @risk_entity_type),
|
|
||||||
@snapshot_id,
|
|
||||||
r.id,
|
|
||||||
r.organization_id,
|
|
||||||
r.name,
|
|
||||||
r.description,
|
|
||||||
r.category,
|
|
||||||
r.treatment,
|
|
||||||
r.note,
|
|
||||||
r.owner_profile_id,
|
|
||||||
r.inherent_likelihood,
|
|
||||||
r.inherent_impact,
|
|
||||||
r.residual_likelihood,
|
|
||||||
r.residual_impact,
|
|
||||||
r.created_at,
|
|
||||||
r.updated_at
|
|
||||||
FROM risks r
|
|
||||||
WHERE %s AND organization_id = @organization_id AND snapshot_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"snapshot_id": snapshotID,
|
|
||||||
"organization_id": organizationID,
|
|
||||||
"risk_entity_type": RiskEntityType,
|
|
||||||
}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, query, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert risk snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,40 +16,24 @@ package coredata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
RiskFilter struct {
|
RiskFilter struct {
|
||||||
query *string
|
query *string
|
||||||
snapshotID **gid.GID
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRiskFilter(query *string, snapshotID **gid.GID) *RiskFilter {
|
func NewRiskFilter(query *string) *RiskFilter {
|
||||||
return &RiskFilter{
|
return &RiskFilter{
|
||||||
query: query,
|
query: query,
|
||||||
snapshotID: snapshotID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *RiskFilter) SQLArguments() pgx.StrictNamedArgs {
|
func (f *RiskFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||||
args := pgx.StrictNamedArgs{
|
return pgx.StrictNamedArgs{
|
||||||
"query": f.query,
|
"query": f.query,
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.snapshotID == nil {
|
|
||||||
args["has_snapshot_filter"] = false
|
|
||||||
args["filter_snapshot_id"] = nil
|
|
||||||
} else if *f.snapshotID == nil {
|
|
||||||
args["has_snapshot_filter"] = true
|
|
||||||
args["filter_snapshot_id"] = nil
|
|
||||||
} else {
|
|
||||||
args["has_snapshot_filter"] = true
|
|
||||||
args["filter_snapshot_id"] = **f.snapshotID
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *RiskFilter) SQLFragment() string {
|
func (f *RiskFilter) SQLFragment() string {
|
||||||
@@ -63,14 +47,5 @@ func (f *RiskFilter) SQLFragment() string {
|
|||||||
)
|
)
|
||||||
ELSE TRUE
|
ELSE TRUE
|
||||||
END
|
END
|
||||||
AND
|
|
||||||
CASE
|
|
||||||
WHEN @has_snapshot_filter::boolean = false THEN TRUE
|
|
||||||
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NOT NULL THEN
|
|
||||||
snapshot_id = @filter_snapshot_id::text
|
|
||||||
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NULL THEN
|
|
||||||
snapshot_id IS NULL
|
|
||||||
ELSE TRUE
|
|
||||||
END
|
|
||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Snapshot struct {
|
|
||||||
ID gid.GID `db:"id"`
|
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
|
||||||
Name string `db:"name"`
|
|
||||||
Description *string `db:"description"`
|
|
||||||
Type SnapshotsType `db:"type"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Snapshots []*Snapshot
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Snapshot) CursorKey(field SnapshotOrderField) page.CursorKey {
|
|
||||||
switch field {
|
|
||||||
case SnapshotOrderFieldCreatedAt:
|
|
||||||
return page.NewCursorKey(s.ID, s.CreatedAt)
|
|
||||||
case SnapshotOrderFieldName:
|
|
||||||
return page.NewCursorKey(s.ID, s.Name)
|
|
||||||
case SnapshotOrderFieldType:
|
|
||||||
return page.NewCursorKey(s.ID, s.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unsupported order by: %s", field))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapshot) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (map[string]string, error) {
|
|
||||||
q := `SELECT organization_id FROM snapshots WHERE id = $1 LIMIT 1;`
|
|
||||||
|
|
||||||
var organizationID gid.GID
|
|
||||||
if err := conn.QueryRow(ctx, q, s.ID).Scan(&organizationID); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return nil, ErrResourceNotFound
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cannot query snapshot authorization attributes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]string{"organization_id": organizationID.String()}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapshot) LoadByID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Querier,
|
|
||||||
scope Scoper,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
type,
|
|
||||||
created_at
|
|
||||||
FROM
|
|
||||||
snapshots
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND id = @snapshot_id
|
|
||||||
LIMIT 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"snapshot_id": snapshotID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Snapshot])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = snapshot
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapshots) CountByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Querier,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
filter *SnapshotFilter,
|
|
||||||
) (int, error) {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
COUNT(id)
|
|
||||||
FROM
|
|
||||||
snapshots
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND type = 'RISKS'
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, filter.SQLArguments())
|
|
||||||
|
|
||||||
row := conn.QueryRow(ctx, q, args)
|
|
||||||
|
|
||||||
var count int
|
|
||||||
if err := row.Scan(&count); err != nil {
|
|
||||||
return 0, fmt.Errorf("cannot scan count: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapshots) LoadByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Querier,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
cursor *page.Cursor[SnapshotOrderField],
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
type,
|
|
||||||
created_at
|
|
||||||
FROM
|
|
||||||
snapshots
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND type = 'RISKS'
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshots, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Snapshot])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = snapshots
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapshot) Insert(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
INSERT INTO snapshots (
|
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
organization_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
type,
|
|
||||||
created_at
|
|
||||||
) VALUES (
|
|
||||||
@id,
|
|
||||||
@tenant_id,
|
|
||||||
@organization_id,
|
|
||||||
@name,
|
|
||||||
@description,
|
|
||||||
@type,
|
|
||||||
@created_at
|
|
||||||
)
|
|
||||||
`
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
|
||||||
"id": s.ID,
|
|
||||||
"tenant_id": scope.GetTenantID(),
|
|
||||||
"organization_id": s.OrganizationID,
|
|
||||||
"name": s.Name,
|
|
||||||
"description": s.Description,
|
|
||||||
"type": s.Type,
|
|
||||||
"created_at": s.CreatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot insert snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapshot) Delete(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Tx,
|
|
||||||
scope Scoper,
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
DELETE FROM snapshots
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND id = @id
|
|
||||||
`
|
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"id": s.ID, "organization_id": s.OrganizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot delete snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snapshots) LoadByControlID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Querier,
|
|
||||||
scope Scoper,
|
|
||||||
controlID gid.GID,
|
|
||||||
cursor *page.Cursor[SnapshotOrderField],
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
WITH snapshots_by_control AS (
|
|
||||||
SELECT
|
|
||||||
s.id,
|
|
||||||
s.tenant_id,
|
|
||||||
s.organization_id,
|
|
||||||
s.name,
|
|
||||||
s.description,
|
|
||||||
s.type,
|
|
||||||
s.created_at
|
|
||||||
FROM
|
|
||||||
snapshots s
|
|
||||||
INNER JOIN
|
|
||||||
controls_snapshots cs ON s.id = cs.snapshot_id
|
|
||||||
WHERE
|
|
||||||
cs.control_id = @control_id
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
type,
|
|
||||||
created_at
|
|
||||||
FROM
|
|
||||||
snapshots_by_control
|
|
||||||
WHERE %s
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshots, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Snapshot])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*s = snapshots
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
SnapshotFilter struct {
|
|
||||||
snapshotType *SnapshotsType
|
|
||||||
beforeDate *time.Time
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewSnapshotFilter(snapshotType *SnapshotsType) *SnapshotFilter {
|
|
||||||
return &SnapshotFilter{
|
|
||||||
snapshotType: snapshotType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *SnapshotFilter) WithBeforeDate(beforeDate *time.Time) *SnapshotFilter {
|
|
||||||
f.beforeDate = beforeDate
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *SnapshotFilter) SQLArguments() pgx.NamedArgs {
|
|
||||||
args := pgx.NamedArgs{
|
|
||||||
"filter_snapshot_type": f.snapshotType,
|
|
||||||
"filter_before_date": f.beforeDate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *SnapshotFilter) SQLFragment() string {
|
|
||||||
return `
|
|
||||||
(
|
|
||||||
CASE
|
|
||||||
WHEN @filter_snapshot_type::snapshots_type IS NOT NULL THEN
|
|
||||||
type = @filter_snapshot_type::snapshots_type
|
|
||||||
ELSE TRUE
|
|
||||||
END
|
|
||||||
AND
|
|
||||||
CASE
|
|
||||||
WHEN @filter_before_date::timestamptz IS NOT NULL THEN
|
|
||||||
created_at <= @filter_before_date::timestamptz
|
|
||||||
ELSE TRUE
|
|
||||||
END
|
|
||||||
)`
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SnapshotOrderField string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SnapshotOrderFieldCreatedAt SnapshotOrderField = "CREATED_AT"
|
|
||||||
SnapshotOrderFieldName SnapshotOrderField = "NAME"
|
|
||||||
SnapshotOrderFieldType SnapshotOrderField = "TYPE"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p SnapshotOrderField) Column() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p SnapshotOrderField) String() string {
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p SnapshotOrderField) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(p.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SnapshotOrderField) UnmarshalText(text []byte) error {
|
|
||||||
val := string(text)
|
|
||||||
switch val {
|
|
||||||
case string(SnapshotOrderFieldCreatedAt),
|
|
||||||
string(SnapshotOrderFieldName),
|
|
||||||
string(SnapshotOrderFieldType):
|
|
||||||
*p = SnapshotOrderField(val)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("invalid SnapshotOrderField value: %q", val)
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
SnapshotsType string
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SnapshotsTypeRisks SnapshotsType = "RISKS"
|
|
||||||
SnapshotsTypeAssets SnapshotsType = "ASSETS"
|
|
||||||
SnapshotsTypeData SnapshotsType = "DATA"
|
|
||||||
SnapshotsTypeFindings SnapshotsType = "FINDINGS"
|
|
||||||
SnapshotsTypeObligations SnapshotsType = "OBLIGATIONS"
|
|
||||||
SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES"
|
|
||||||
SnapshotsTypeStatementsOfApplicability SnapshotsType = "STATEMENTS_OF_APPLICABILITY"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SnapshotsTypes() []SnapshotsType {
|
|
||||||
return []SnapshotsType{
|
|
||||||
SnapshotsTypeRisks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st SnapshotsType) String() string {
|
|
||||||
return string(st)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st *SnapshotsType) Scan(value any) error {
|
|
||||||
var s string
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
s = v
|
|
||||||
case []byte:
|
|
||||||
s = string(v)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type for SnapshotsType: %T", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s {
|
|
||||||
case SnapshotsTypeRisks.String():
|
|
||||||
*st = SnapshotsTypeRisks
|
|
||||||
case SnapshotsTypeAssets.String():
|
|
||||||
*st = SnapshotsTypeAssets
|
|
||||||
case SnapshotsTypeData.String():
|
|
||||||
*st = SnapshotsTypeData
|
|
||||||
case SnapshotsTypeFindings.String(), "NONCONFORMITIES", "CONTINUAL_IMPROVEMENTS":
|
|
||||||
*st = SnapshotsTypeFindings
|
|
||||||
case SnapshotsTypeObligations.String():
|
|
||||||
*st = SnapshotsTypeObligations
|
|
||||||
case SnapshotsTypeProcessingActivities.String():
|
|
||||||
*st = SnapshotsTypeProcessingActivities
|
|
||||||
case SnapshotsTypeStatementsOfApplicability.String():
|
|
||||||
*st = SnapshotsTypeStatementsOfApplicability
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid SnapshotsType value: %q", s)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st SnapshotsType) Value() (driver.Value, error) {
|
|
||||||
return st.String(), nil
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package coredata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Snapshottable interface {
|
|
||||||
Snapshot(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
|
|
||||||
switch snapshotType {
|
|
||||||
case SnapshotsTypeRisks:
|
|
||||||
return Risks{}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -259,6 +259,35 @@ type (
|
|||||||
Vendors string
|
Vendors string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RiskListData struct {
|
||||||
|
Title string
|
||||||
|
OrganizationName string
|
||||||
|
CreatedAt time.Time
|
||||||
|
TotalRisks int
|
||||||
|
Rows []RiskListRow
|
||||||
|
}
|
||||||
|
|
||||||
|
RiskListRow struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Category string
|
||||||
|
Treatment string
|
||||||
|
Owner string
|
||||||
|
InherentLikelihood int
|
||||||
|
InherentLikelihoodLabel string
|
||||||
|
InherentImpact int
|
||||||
|
InherentImpactLabel string
|
||||||
|
InherentRiskScore int
|
||||||
|
InherentSeverity string
|
||||||
|
ResidualLikelihood int
|
||||||
|
ResidualLikelihoodLabel string
|
||||||
|
ResidualImpact int
|
||||||
|
ResidualImpactLabel string
|
||||||
|
ResidualRiskScore int
|
||||||
|
ResidualSeverity string
|
||||||
|
Note string
|
||||||
|
}
|
||||||
|
|
||||||
FindingListData struct {
|
FindingListData struct {
|
||||||
Title string
|
Title string
|
||||||
OrganizationName string
|
OrganizationName string
|
||||||
|
|||||||
@@ -149,8 +149,6 @@ const (
|
|||||||
ActionControlDocumentMappingDelete = "core:control:delete-document-mapping"
|
ActionControlDocumentMappingDelete = "core:control:delete-document-mapping"
|
||||||
ActionControlAuditMappingCreate = "core:control:create-audit-mapping"
|
ActionControlAuditMappingCreate = "core:control:create-audit-mapping"
|
||||||
ActionControlAuditMappingDelete = "core:control:delete-audit-mapping"
|
ActionControlAuditMappingDelete = "core:control:delete-audit-mapping"
|
||||||
ActionControlSnapshotMappingCreate = "core:control:create-snapshot-mapping"
|
|
||||||
ActionControlSnapshotMappingDelete = "core:control:delete-snapshot-mapping"
|
|
||||||
ActionControlObligationMappingCreate = "core:control:create-obligation-mapping"
|
ActionControlObligationMappingCreate = "core:control:create-obligation-mapping"
|
||||||
ActionControlObligationMappingDelete = "core:control:delete-obligation-mapping"
|
ActionControlObligationMappingDelete = "core:control:delete-obligation-mapping"
|
||||||
|
|
||||||
@@ -226,6 +224,7 @@ const (
|
|||||||
ActionRiskDocumentMappingDelete = "core:risk:delete-document-mapping"
|
ActionRiskDocumentMappingDelete = "core:risk:delete-document-mapping"
|
||||||
ActionRiskObligationMappingCreate = "core:risk:create-obligation-mapping"
|
ActionRiskObligationMappingCreate = "core:risk:create-obligation-mapping"
|
||||||
ActionRiskObligationMappingDelete = "core:risk:delete-obligation-mapping"
|
ActionRiskObligationMappingDelete = "core:risk:delete-obligation-mapping"
|
||||||
|
ActionRiskPublish = "core:risk:publish"
|
||||||
|
|
||||||
// Asset actions
|
// Asset actions
|
||||||
ActionAssetGet = "core:asset:get"
|
ActionAssetGet = "core:asset:get"
|
||||||
@@ -283,12 +282,6 @@ const (
|
|||||||
ActionProcessingActivityDelete = "core:processing-activity:delete"
|
ActionProcessingActivityDelete = "core:processing-activity:delete"
|
||||||
ActionProcessingActivityPublish = "core:processing-activity:publish"
|
ActionProcessingActivityPublish = "core:processing-activity:publish"
|
||||||
|
|
||||||
// Snapshot actions
|
|
||||||
ActionSnapshotGet = "core:snapshot:get"
|
|
||||||
ActionSnapshotList = "core:snapshot:list"
|
|
||||||
ActionSnapshotCreate = "core:snapshot:create"
|
|
||||||
ActionSnapshotDelete = "core:snapshot:delete"
|
|
||||||
|
|
||||||
// CustomDomain actions
|
// CustomDomain actions
|
||||||
ActionCustomDomainGet = "core:custom-domain:get"
|
ActionCustomDomainGet = "core:custom-domain:get"
|
||||||
ActionCustomDomainCreate = "core:custom-domain:create"
|
ActionCustomDomainCreate = "core:custom-domain:create"
|
||||||
|
|||||||
@@ -707,112 +707,6 @@ func (s ControlService) ListForAuditID(
|
|||||||
return page.NewPage([]*coredata.Control(controls), cursor), nil
|
return page.NewPage([]*coredata.Control(controls), cursor), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s ControlService) CreateSnapshotMapping(
|
|
||||||
ctx context.Context,
|
|
||||||
controlID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) (*coredata.Control, *coredata.Snapshot, error) {
|
|
||||||
control := &coredata.Control{}
|
|
||||||
snapshot := &coredata.Snapshot{}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
|
||||||
if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load control: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
controlSnapshot := &coredata.ControlSnapshot{
|
|
||||||
ControlID: controlID,
|
|
||||||
SnapshotID: snapshotID,
|
|
||||||
OrganizationID: control.OrganizationID,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := snapshot.LoadByID(ctx, conn, s.svc.scope, snapshotID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := controlSnapshot.Upsert(ctx, conn, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot create control snapshot mapping: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return control, snapshot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s ControlService) DeleteSnapshotMapping(
|
|
||||||
ctx context.Context,
|
|
||||||
controlID gid.GID,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) (*coredata.Control, *coredata.Snapshot, error) {
|
|
||||||
control := &coredata.Control{}
|
|
||||||
snapshot := &coredata.Snapshot{}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, tx pg.Tx) error {
|
|
||||||
if err := control.LoadByID(ctx, tx, s.svc.scope, controlID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load control: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := snapshot.LoadByID(ctx, tx, s.svc.scope, snapshotID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
controlSnapshot := &coredata.ControlSnapshot{}
|
|
||||||
if err := controlSnapshot.Delete(ctx, tx, s.svc.scope, control.ID, snapshot.ID); err != nil {
|
|
||||||
return fmt.Errorf("cannot delete control snapshot mapping: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot delete control snapshot mapping: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return control, snapshot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s ControlService) ListForSnapshotID(
|
|
||||||
ctx context.Context,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
cursor *page.Cursor[coredata.ControlOrderField],
|
|
||||||
filter *coredata.ControlFilter,
|
|
||||||
) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) {
|
|
||||||
var controls coredata.Controls
|
|
||||||
snapshot := &coredata.Snapshot{}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
|
||||||
if err := snapshot.LoadByID(ctx, conn, s.svc.scope, snapshotID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load snapshot: %w", err)
|
|
||||||
}
|
|
||||||
if err := controls.LoadBySnapshotID(ctx, conn, s.svc.scope, snapshotID, cursor, filter); err != nil {
|
|
||||||
return fmt.Errorf("cannot load controls: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.NewPage([]*coredata.Control(controls), cursor), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s ControlService) CountForStatementOfApplicabilityID(
|
func (s ControlService) CountForStatementOfApplicabilityID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
statementOfApplicabilityID gid.GID,
|
statementOfApplicabilityID gid.GID,
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ func (s *GeneratedDocumentService) PublishStatementOfApplicability(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasApprovers := len(approverIDs) > 0
|
|
||||||
|
|
||||||
if existingDoc == nil {
|
if existingDoc == nil {
|
||||||
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
|
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
|
||||||
|
|
||||||
@@ -111,16 +109,11 @@ func (s *GeneratedDocumentService) PublishStatementOfApplicability(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
if hasApprovers {
|
if len(approverIDs) > 0 {
|
||||||
versionStatus = coredata.DocumentVersionStatusDraft
|
versionStatus = coredata.DocumentVersionStatusDraft
|
||||||
} else {
|
} else {
|
||||||
publishedAt = &now
|
publishedAt = &now
|
||||||
@@ -144,41 +137,7 @@ func (s *GeneratedDocumentService) PublishStatementOfApplicability(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, soa.OrganizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, soa.OrganizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -415,12 +374,7 @@ func (s *GeneratedDocumentService) PublishDataList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -435,7 +389,7 @@ func (s *GeneratedDocumentService) PublishDataList(
|
|||||||
ID: documentVersionID,
|
ID: documentVersionID,
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
DocumentID: document.ID,
|
DocumentID: document.ID,
|
||||||
Title: "Data List",
|
Title: "Data",
|
||||||
Major: newMajor,
|
Major: newMajor,
|
||||||
Minor: 0,
|
Minor: 0,
|
||||||
Content: prosemirrorJSON,
|
Content: prosemirrorJSON,
|
||||||
@@ -448,41 +402,7 @@ func (s *GeneratedDocumentService) PublishDataList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -524,7 +444,7 @@ func (s *GeneratedDocumentService) buildDataListDocumentData(
|
|||||||
|
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return docgen.DataListData{
|
return docgen.DataListData{
|
||||||
Title: "Data List",
|
Title: "Data",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalData: 0,
|
TotalData: 0,
|
||||||
@@ -581,7 +501,7 @@ func (s *GeneratedDocumentService) buildDataListDocumentData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return docgen.DataListData{
|
return docgen.DataListData{
|
||||||
Title: "Data List",
|
Title: "Data",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalData: len(data),
|
TotalData: len(data),
|
||||||
@@ -705,12 +625,7 @@ func (s *GeneratedDocumentService) PublishAssetList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -725,7 +640,7 @@ func (s *GeneratedDocumentService) PublishAssetList(
|
|||||||
ID: documentVersionID,
|
ID: documentVersionID,
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
DocumentID: document.ID,
|
DocumentID: document.ID,
|
||||||
Title: "Asset List",
|
Title: "Assets",
|
||||||
Major: newMajor,
|
Major: newMajor,
|
||||||
Minor: 0,
|
Minor: 0,
|
||||||
Content: prosemirrorJSON,
|
Content: prosemirrorJSON,
|
||||||
@@ -738,41 +653,7 @@ func (s *GeneratedDocumentService) PublishAssetList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -814,7 +695,7 @@ func (s *GeneratedDocumentService) buildAssetListDocumentData(
|
|||||||
|
|
||||||
if len(assets) == 0 {
|
if len(assets) == 0 {
|
||||||
return docgen.AssetListData{
|
return docgen.AssetListData{
|
||||||
Title: "Asset List",
|
Title: "Assets",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalAssets: 0,
|
TotalAssets: 0,
|
||||||
@@ -873,7 +754,7 @@ func (s *GeneratedDocumentService) buildAssetListDocumentData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return docgen.AssetListData{
|
return docgen.AssetListData{
|
||||||
Title: "Asset List",
|
Title: "Assets",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalAssets: len(assets),
|
TotalAssets: len(assets),
|
||||||
@@ -1016,12 +897,7 @@ func (s *GeneratedDocumentService) PublishFindingList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -1036,7 +912,7 @@ func (s *GeneratedDocumentService) PublishFindingList(
|
|||||||
ID: documentVersionID,
|
ID: documentVersionID,
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
DocumentID: document.ID,
|
DocumentID: document.ID,
|
||||||
Title: "Finding List",
|
Title: "Findings",
|
||||||
Major: newMajor,
|
Major: newMajor,
|
||||||
Minor: 0,
|
Minor: 0,
|
||||||
Content: prosemirrorJSON,
|
Content: prosemirrorJSON,
|
||||||
@@ -1049,41 +925,7 @@ func (s *GeneratedDocumentService) PublishFindingList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1125,7 +967,7 @@ func (s *GeneratedDocumentService) buildFindingListDocumentData(
|
|||||||
|
|
||||||
if len(findings) == 0 {
|
if len(findings) == 0 {
|
||||||
return docgen.FindingListData{
|
return docgen.FindingListData{
|
||||||
Title: "Finding List",
|
Title: "Findings",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalFindings: 0,
|
TotalFindings: 0,
|
||||||
@@ -1216,7 +1058,7 @@ func (s *GeneratedDocumentService) buildFindingListDocumentData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return docgen.FindingListData{
|
return docgen.FindingListData{
|
||||||
Title: "Finding List",
|
Title: "Findings",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalFindings: len(findings),
|
TotalFindings: len(findings),
|
||||||
@@ -1372,12 +1214,7 @@ func (s *GeneratedDocumentService) PublishObligationList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -1392,7 +1229,7 @@ func (s *GeneratedDocumentService) PublishObligationList(
|
|||||||
ID: documentVersionID,
|
ID: documentVersionID,
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
DocumentID: document.ID,
|
DocumentID: document.ID,
|
||||||
Title: "Obligation List",
|
Title: "Obligations",
|
||||||
Major: newMajor,
|
Major: newMajor,
|
||||||
Minor: 0,
|
Minor: 0,
|
||||||
Content: prosemirrorJSON,
|
Content: prosemirrorJSON,
|
||||||
@@ -1405,41 +1242,7 @@ func (s *GeneratedDocumentService) PublishObligationList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1481,7 +1284,7 @@ func (s *GeneratedDocumentService) buildObligationListDocumentData(
|
|||||||
|
|
||||||
if len(obligations) == 0 {
|
if len(obligations) == 0 {
|
||||||
return docgen.ObligationListData{
|
return docgen.ObligationListData{
|
||||||
Title: "Obligation List",
|
Title: "Obligations",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalObligations: 0,
|
TotalObligations: 0,
|
||||||
@@ -1563,7 +1366,7 @@ func (s *GeneratedDocumentService) buildObligationListDocumentData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return docgen.ObligationListData{
|
return docgen.ObligationListData{
|
||||||
Title: "Obligation List",
|
Title: "Obligations",
|
||||||
OrganizationName: organization.Name,
|
OrganizationName: organization.Name,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
TotalObligations: len(obligations),
|
TotalObligations: len(obligations),
|
||||||
@@ -1696,12 +1499,7 @@ func (s *GeneratedDocumentService) PublishProcessingActivityList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -1729,41 +1527,7 @@ func (s *GeneratedDocumentService) PublishProcessingActivityList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2115,12 +1879,7 @@ func (s *GeneratedDocumentService) PublishDataProtectionImpactAssessmentList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -2148,41 +1907,7 @@ func (s *GeneratedDocumentService) PublishDataProtectionImpactAssessmentList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2379,12 +2104,7 @@ func (s *GeneratedDocumentService) PublishTransferImpactAssessmentList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -2412,41 +2132,7 @@ func (s *GeneratedDocumentService) PublishTransferImpactAssessmentList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = new(0)
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2656,12 +2342,7 @@ func (s *GeneratedDocumentService) PublishVendorList(
|
|||||||
document = existingDoc
|
document = existingDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
var newMajor int
|
newMajor := nextDocumentMajor(document)
|
||||||
if document.CurrentPublishedMajor != nil {
|
|
||||||
newMajor = *document.CurrentPublishedMajor + 1
|
|
||||||
} else {
|
|
||||||
newMajor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStatus := coredata.DocumentVersionStatusPublished
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
var publishedAt *time.Time
|
var publishedAt *time.Time
|
||||||
@@ -2689,42 +2370,7 @@ func (s *GeneratedDocumentService) PublishVendorList(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
|
||||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cannot insert document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasApprovers {
|
|
||||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
|
||||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
|
||||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
|
||||||
ctx,
|
|
||||||
tx,
|
|
||||||
document,
|
|
||||||
documentVersion,
|
|
||||||
approverIDs,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot request approval: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
zero := 0
|
|
||||||
document.CurrentPublishedMajor = &newMajor
|
|
||||||
document.CurrentPublishedMinor = &zero
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3096,3 +2742,350 @@ func BuildVendorListDocument(data docgen.VendorListData) (string, error) {
|
|||||||
}
|
}
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var riskListTemplate = template.Must(
|
||||||
|
template.New("risk_list.json.tmpl").
|
||||||
|
Funcs(template.FuncMap{
|
||||||
|
"json": func(v any) (string, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
},
|
||||||
|
"printf": fmt.Sprintf,
|
||||||
|
"add": func(a, b int) int { return a + b },
|
||||||
|
}).
|
||||||
|
ParseFS(Templates, "templates/risk_list.json.tmpl"),
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildRiskListDocument(data docgen.RiskListData) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := riskListTemplate.Execute(&buf, data); err != nil {
|
||||||
|
return "", fmt.Errorf("cannot execute risk list template: %w", err)
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeneratedDocumentService) PublishRiskList(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
approverIDs []gid.GID,
|
||||||
|
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||||
|
var (
|
||||||
|
document *coredata.Document
|
||||||
|
documentVersion *coredata.DocumentVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
organization := &coredata.Organization{}
|
||||||
|
if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
documentData, err := s.buildRiskListDocumentData(ctx, tx, organization)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build document data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prosemirrorJSON, err := BuildRiskListDocument(documentData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build prosemirror document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
risk := coredata.Risk{}
|
||||||
|
riskDocumentID, err := risk.GetGeneratedDocumentID(ctx, tx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query generated documents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDoc *coredata.Document
|
||||||
|
if riskDocumentID != nil {
|
||||||
|
doc := &coredata.Document{}
|
||||||
|
err = doc.LoadByID(ctx, tx, s.svc.scope, *riskDocumentID)
|
||||||
|
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return fmt.Errorf("cannot load risk list document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && doc.ArchivedAt == nil {
|
||||||
|
existingDoc = doc
|
||||||
|
} else {
|
||||||
|
if err := risk.ClearGeneratedDocumentID(ctx, tx, []gid.GID{*riskDocumentID}); err != nil {
|
||||||
|
return fmt.Errorf("cannot clear document reference: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasApprovers := len(approverIDs) > 0
|
||||||
|
|
||||||
|
if existingDoc == nil {
|
||||||
|
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
|
||||||
|
|
||||||
|
document = &coredata.Document{
|
||||||
|
ID: documentID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
WriteMode: coredata.DocumentWriteModeGenerated,
|
||||||
|
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
|
||||||
|
Status: coredata.DocumentStatusActive,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := document.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := risk.UpsertGeneratedDocumentID(ctx, tx, organizationID, s.svc.scope.GetTenantID(), documentID); err != nil {
|
||||||
|
return fmt.Errorf("cannot upsert generated documents: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document = existingDoc
|
||||||
|
}
|
||||||
|
|
||||||
|
newMajor := nextDocumentMajor(document)
|
||||||
|
|
||||||
|
versionStatus := coredata.DocumentVersionStatusPublished
|
||||||
|
var publishedAt *time.Time
|
||||||
|
if hasApprovers {
|
||||||
|
versionStatus = coredata.DocumentVersionStatusDraft
|
||||||
|
} else {
|
||||||
|
publishedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
documentVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType)
|
||||||
|
documentVersion = &coredata.DocumentVersion{
|
||||||
|
ID: documentVersionID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
DocumentID: document.ID,
|
||||||
|
Title: "Risks",
|
||||||
|
Major: newMajor,
|
||||||
|
Minor: 0,
|
||||||
|
Content: prosemirrorJSON,
|
||||||
|
Status: versionStatus,
|
||||||
|
Classification: coredata.DocumentClassificationConfidential,
|
||||||
|
DocumentType: coredata.DocumentTypeRegister,
|
||||||
|
Orientation: coredata.DocumentVersionOrientationPortrait,
|
||||||
|
PublishedAt: publishedAt,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.publishOrRequestApproval(ctx, tx, document, documentVersion, organizationID, approverIDs, newMajor, now)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeneratedDocumentService) GetRisksDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) (*gid.GID, error) {
|
||||||
|
var riskDocumentID *gid.GID
|
||||||
|
|
||||||
|
err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
risk := coredata.Risk{}
|
||||||
|
var err error
|
||||||
|
riskDocumentID, err = risk.GetGeneratedDocumentID(ctx, conn, organizationID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get risk list document ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return riskDocumentID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GeneratedDocumentService) buildRiskListDocumentData(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
organization *coredata.Organization,
|
||||||
|
) (docgen.RiskListData, error) {
|
||||||
|
var risks coredata.Risks
|
||||||
|
if err := risks.LoadAllByOrganizationID(ctx, conn, s.svc.scope, organization.ID); err != nil {
|
||||||
|
return docgen.RiskListData{}, fmt.Errorf("cannot load risks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(risks) == 0 {
|
||||||
|
return docgen.RiskListData{
|
||||||
|
Title: "Risks",
|
||||||
|
OrganizationName: organization.Name,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
TotalRisks: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerIDs := make([]gid.GID, 0, len(risks))
|
||||||
|
ownerIDSet := make(map[gid.GID]struct{})
|
||||||
|
for _, r := range risks {
|
||||||
|
if r.OwnerID != nil {
|
||||||
|
if _, ok := ownerIDSet[*r.OwnerID]; !ok {
|
||||||
|
ownerIDs = append(ownerIDs, *r.OwnerID)
|
||||||
|
ownerIDSet[*r.OwnerID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileMap := make(map[gid.GID]*coredata.MembershipProfile)
|
||||||
|
if len(ownerIDs) > 0 {
|
||||||
|
var profiles coredata.MembershipProfiles
|
||||||
|
if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil {
|
||||||
|
return docgen.RiskListData{}, fmt.Errorf("cannot load profiles: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range profiles {
|
||||||
|
profileMap[p.ID] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]docgen.RiskListRow, 0, len(risks))
|
||||||
|
for _, r := range risks {
|
||||||
|
rows = append(rows, docgen.RiskListRow{
|
||||||
|
Name: r.Name,
|
||||||
|
Description: derefStringOrNotSpecified(r.Description),
|
||||||
|
Category: stringOrNotSpecified(r.Category),
|
||||||
|
Treatment: formatRiskTreatment(r.Treatment),
|
||||||
|
Owner: lookupProfileName(profileMap, r.OwnerID),
|
||||||
|
InherentLikelihood: r.InherentLikelihood,
|
||||||
|
InherentLikelihoodLabel: riskLikelihoodLabel(r.InherentLikelihood),
|
||||||
|
InherentImpact: r.InherentImpact,
|
||||||
|
InherentImpactLabel: riskImpactLabel(r.InherentImpact),
|
||||||
|
InherentRiskScore: r.InherentRiskScore,
|
||||||
|
InherentSeverity: riskSeverityLabel(r.InherentRiskScore),
|
||||||
|
ResidualLikelihood: r.ResidualLikelihood,
|
||||||
|
ResidualLikelihoodLabel: riskLikelihoodLabel(r.ResidualLikelihood),
|
||||||
|
ResidualImpact: r.ResidualImpact,
|
||||||
|
ResidualImpactLabel: riskImpactLabel(r.ResidualImpact),
|
||||||
|
ResidualRiskScore: r.ResidualRiskScore,
|
||||||
|
ResidualSeverity: riskSeverityLabel(r.ResidualRiskScore),
|
||||||
|
Note: stringOrNotSpecified(r.Note),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return docgen.RiskListData{
|
||||||
|
Title: "Risks",
|
||||||
|
OrganizationName: organization.Name,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
TotalRisks: len(risks),
|
||||||
|
Rows: rows,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskLikelihoodLabel(v int) string {
|
||||||
|
switch v {
|
||||||
|
case 1:
|
||||||
|
return "Improbable"
|
||||||
|
case 2:
|
||||||
|
return "Remote"
|
||||||
|
case 3:
|
||||||
|
return "Occasional"
|
||||||
|
case 4:
|
||||||
|
return "Probable"
|
||||||
|
case 5:
|
||||||
|
return "Frequent"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskImpactLabel(v int) string {
|
||||||
|
switch v {
|
||||||
|
case 1:
|
||||||
|
return "Negligible"
|
||||||
|
case 2:
|
||||||
|
return "Low"
|
||||||
|
case 3:
|
||||||
|
return "Moderate"
|
||||||
|
case 4:
|
||||||
|
return "Significant"
|
||||||
|
case 5:
|
||||||
|
return "Catastrophic"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskSeverityLabel(score int) string {
|
||||||
|
switch {
|
||||||
|
case score >= 15:
|
||||||
|
return "Critical"
|
||||||
|
case score >= 5:
|
||||||
|
return "High"
|
||||||
|
default:
|
||||||
|
return "Low"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRiskTreatment(t coredata.RiskTreatment) string {
|
||||||
|
switch t {
|
||||||
|
case coredata.RiskTreatmentMitigated:
|
||||||
|
return "Mitigated"
|
||||||
|
case coredata.RiskTreatmentAccepted:
|
||||||
|
return "Accepted"
|
||||||
|
case coredata.RiskTreatmentAvoided:
|
||||||
|
return "Avoided"
|
||||||
|
case coredata.RiskTreatmentTransferred:
|
||||||
|
return "Transferred"
|
||||||
|
default:
|
||||||
|
return stringOrNotSpecified(string(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextDocumentMajor returns the major version to use for a new published
|
||||||
|
// version of a generated document.
|
||||||
|
func nextDocumentMajor(doc *coredata.Document) int {
|
||||||
|
if doc.CurrentPublishedMajor != nil {
|
||||||
|
return *doc.CurrentPublishedMajor + 1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishOrRequestApproval inserts a freshly built generated document version
|
||||||
|
// and either requests approval (if approverIDs is non-empty) or marks the
|
||||||
|
// document as currently published at newMajor.0. The pending-approval insert
|
||||||
|
// conflict is mapped to a friendlier error.
|
||||||
|
func (s *GeneratedDocumentService) publishOrRequestApproval(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
document *coredata.Document,
|
||||||
|
version *coredata.DocumentVersion,
|
||||||
|
organizationID gid.GID,
|
||||||
|
approverIDs []gid.GID,
|
||||||
|
newMajor int,
|
||||||
|
now time.Time,
|
||||||
|
) error {
|
||||||
|
if err := version.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||||
|
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot insert document version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(approverIDs) > 0 {
|
||||||
|
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
||||||
|
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
||||||
|
return fmt.Errorf("cannot save default approvers: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := s.svc.DocumentApprovals.RequestApprovalInTx(ctx, tx, document, version, approverIDs, nil); err != nil {
|
||||||
|
return fmt.Errorf("cannot request approval: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
document.CurrentPublishedMajor = &newMajor
|
||||||
|
document.CurrentPublishedMinor = new(0)
|
||||||
|
document.UpdatedAt = now
|
||||||
|
|
||||||
|
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update document: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ var ViewerPolicy = policy.NewPolicy(
|
|||||||
ActionProcessingActivityGet, ActionProcessingActivityList,
|
ActionProcessingActivityGet, ActionProcessingActivityList,
|
||||||
ActionDataProtectionImpactAssessmentGet, ActionDataProtectionImpactAssessmentList,
|
ActionDataProtectionImpactAssessmentGet, ActionDataProtectionImpactAssessmentList,
|
||||||
ActionTransferImpactAssessmentGet, ActionTransferImpactAssessmentList,
|
ActionTransferImpactAssessmentGet, ActionTransferImpactAssessmentList,
|
||||||
ActionSnapshotGet, ActionSnapshotList,
|
|
||||||
ActionFileGet, ActionFileDownloadUrl,
|
ActionFileGet, ActionFileDownloadUrl,
|
||||||
ActionSlackConnectionList, ActionConnectorList,
|
ActionSlackConnectionList, ActionConnectorList,
|
||||||
ActionRightsRequestGet, ActionRightsRequestList,
|
ActionRightsRequestGet, ActionRightsRequestList,
|
||||||
@@ -152,7 +151,6 @@ var AuditorPolicy = policy.NewPolicy(
|
|||||||
ActionProcessingActivityGet, ActionProcessingActivityList,
|
ActionProcessingActivityGet, ActionProcessingActivityList,
|
||||||
ActionDataProtectionImpactAssessmentGet, ActionDataProtectionImpactAssessmentList,
|
ActionDataProtectionImpactAssessmentGet, ActionDataProtectionImpactAssessmentList,
|
||||||
ActionTransferImpactAssessmentGet, ActionTransferImpactAssessmentList,
|
ActionTransferImpactAssessmentGet, ActionTransferImpactAssessmentList,
|
||||||
ActionSnapshotGet, ActionSnapshotList,
|
|
||||||
ActionFileGet, ActionFileDownloadUrl,
|
ActionFileGet, ActionFileDownloadUrl,
|
||||||
ActionStatementOfApplicabilityGet, ActionStatementOfApplicabilityList,
|
ActionStatementOfApplicabilityGet, ActionStatementOfApplicabilityList,
|
||||||
ActionApplicabilityStatementGet, ActionApplicabilityStatementList,
|
ActionApplicabilityStatementGet, ActionApplicabilityStatementList,
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ type (
|
|||||||
ComplianceExternalURLs *ComplianceExternalURLService
|
ComplianceExternalURLs *ComplianceExternalURLService
|
||||||
Findings *FindingService
|
Findings *FindingService
|
||||||
Obligations *ObligationService
|
Obligations *ObligationService
|
||||||
Snapshots *SnapshotService
|
|
||||||
RightsRequests *RightsRequestService
|
RightsRequests *RightsRequestService
|
||||||
ProcessingActivities *ProcessingActivityService
|
ProcessingActivities *ProcessingActivityService
|
||||||
DataProtectionImpactAssessments *DataProtectionImpactAssessmentService
|
DataProtectionImpactAssessments *DataProtectionImpactAssessmentService
|
||||||
@@ -278,7 +277,6 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
|
|||||||
}
|
}
|
||||||
tenantService.Findings = &FindingService{svc: tenantService}
|
tenantService.Findings = &FindingService{svc: tenantService}
|
||||||
tenantService.Obligations = &ObligationService{svc: tenantService}
|
tenantService.Obligations = &ObligationService{svc: tenantService}
|
||||||
tenantService.Snapshots = &SnapshotService{svc: tenantService}
|
|
||||||
tenantService.RightsRequests = &RightsRequestService{svc: tenantService}
|
tenantService.RightsRequests = &RightsRequestService{svc: tenantService}
|
||||||
tenantService.ProcessingActivities = &ProcessingActivityService{
|
tenantService.ProcessingActivities = &ProcessingActivityService{
|
||||||
svc: tenantService,
|
svc: tenantService,
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package probo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"go.probo.inc/probo/pkg/validator"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SnapshotService struct {
|
|
||||||
svc *TenantService
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateSnapshotRequest struct {
|
|
||||||
OrganizationID gid.GID
|
|
||||||
Name string
|
|
||||||
Description *string
|
|
||||||
Type coredata.SnapshotsType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (csr *CreateSnapshotRequest) Validate() error {
|
|
||||||
v := validator.New()
|
|
||||||
|
|
||||||
v.Check(csr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
|
|
||||||
v.Check(csr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
|
||||||
v.Check(csr.Description, "description", validator.SafeText(ContentMaxLength))
|
|
||||||
v.Check(csr.Type, "type", validator.Required(), validator.OneOfSlice(coredata.SnapshotsTypes()))
|
|
||||||
|
|
||||||
return v.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SnapshotService) Get(
|
|
||||||
ctx context.Context,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) (*coredata.Snapshot, error) {
|
|
||||||
snapshot := &coredata.Snapshot{}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
|
||||||
return snapshot.LoadByID(ctx, conn, s.svc.scope, snapshotID)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SnapshotService) Create(
|
|
||||||
ctx context.Context,
|
|
||||||
req *CreateSnapshotRequest,
|
|
||||||
) (*coredata.Snapshot, error) {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
snapshot := &coredata.Snapshot{
|
|
||||||
ID: gid.New(s.svc.scope.GetTenantID(), coredata.SnapshotEntityType),
|
|
||||||
OrganizationID: req.OrganizationID,
|
|
||||||
Name: req.Name,
|
|
||||||
Description: req.Description,
|
|
||||||
Type: req.Type,
|
|
||||||
CreatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Tx) error {
|
|
||||||
organization := &coredata.Organization{}
|
|
||||||
if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load organization: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := snapshot.Insert(ctx, conn, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot insert snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshottable, err := coredata.GetSnapshottable(req.Type)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := snapshottable.Snapshot(ctx, conn, s.svc.scope, req.OrganizationID, snapshot.ID); err != nil {
|
|
||||||
return fmt.Errorf("cannot create snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SnapshotService) Delete(
|
|
||||||
ctx context.Context,
|
|
||||||
snapshotID gid.GID,
|
|
||||||
) error {
|
|
||||||
err := s.svc.pg.WithTx(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, tx pg.Tx) error {
|
|
||||||
snapshot := &coredata.Snapshot{}
|
|
||||||
if err := snapshot.LoadByID(ctx, tx, s.svc.scope, snapshotID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := snapshot.Delete(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot delete snapshot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SnapshotService) ListForOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
organizationID gid.GID,
|
|
||||||
cursor *page.Cursor[coredata.SnapshotOrderField],
|
|
||||||
) (*page.Page[*coredata.Snapshot, coredata.SnapshotOrderField], error) {
|
|
||||||
snapshots := coredata.Snapshots{}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
|
||||||
if err := snapshots.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor); err != nil {
|
|
||||||
return fmt.Errorf("cannot load snapshots: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.NewPage(snapshots, cursor), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SnapshotService) CountForOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
organizationID gid.GID,
|
|
||||||
) (int, error) {
|
|
||||||
var count int
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
|
||||||
snapshots := coredata.Snapshots{}
|
|
||||||
filter := coredata.NewSnapshotFilter(nil)
|
|
||||||
count, err = snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot count snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SnapshotService) ListForControlID(
|
|
||||||
ctx context.Context,
|
|
||||||
controlID gid.GID,
|
|
||||||
cursor *page.Cursor[coredata.SnapshotOrderField],
|
|
||||||
) (*page.Page[*coredata.Snapshot, coredata.SnapshotOrderField], error) {
|
|
||||||
var snapshots coredata.Snapshots
|
|
||||||
control := &coredata.Control{}
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
|
||||||
if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load control: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := snapshots.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot load snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.NewPage(snapshots, cursor), nil
|
|
||||||
}
|
|
||||||
178
pkg/probo/templates/risk_list.json.tmpl
Normal file
178
pkg/probo/templates/risk_list.json.tmpl
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"type": "doc",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 1 },
|
||||||
|
"content": [{ "type": "text", "text": "1. Purpose" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": "This document provides a comprehensive register of all risks identified within the organization. It captures each risk's classification, treatment, ownership, inherent and residual scoring (likelihood × impact), the related controls, measures, documents and obligations, plus any relevant notes." }]
|
||||||
|
},
|
||||||
|
{ "type": "horizontalRule" },
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 1 },
|
||||||
|
"content": [{ "type": "text", "text": "2. Risks" }]
|
||||||
|
}{{range $i, $r := .Rows}},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 2 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d %s" (add $i 1) $r.Name)}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.1 General Information" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Description: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Description}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Category: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Category}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Owner: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Owner}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{ "type": "text", "text": "Treatment: ", "marks": [{ "type": "bold" }] },
|
||||||
|
{ "type": "text", "text": {{json $r.Treatment}} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.2 Inherent Risk" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "tableRow",
|
||||||
|
"content": [
|
||||||
|
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Likelihood", "marks": [{ "type": "bold" }] }] }] },
|
||||||
|
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Impact", "marks": [{ "type": "bold" }] }] }] },
|
||||||
|
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Score", "marks": [{ "type": "bold" }] }] }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tableRow",
|
||||||
|
"content": [
|
||||||
|
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json (printf "%d — %s" $r.InherentLikelihood $r.InherentLikelihoodLabel)}} }] }] },
|
||||||
|
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json (printf "%d — %s" $r.InherentImpact $r.InherentImpactLabel)}} }] }] },
|
||||||
|
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json (printf "%d — %s" $r.InherentRiskScore $r.InherentSeverity)}} }] }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.3 Residual Risk" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "tableRow",
|
||||||
|
"content": [
|
||||||
|
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Likelihood", "marks": [{ "type": "bold" }] }] }] },
|
||||||
|
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Impact", "marks": [{ "type": "bold" }] }] }] },
|
||||||
|
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Score", "marks": [{ "type": "bold" }] }] }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tableRow",
|
||||||
|
"content": [
|
||||||
|
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json (printf "%d — %s" $r.ResidualLikelihood $r.ResidualLikelihoodLabel)}} }] }] },
|
||||||
|
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json (printf "%d — %s" $r.ResidualImpact $r.ResidualImpactLabel)}} }] }] },
|
||||||
|
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [160] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json (printf "%d — %s" $r.ResidualRiskScore $r.ResidualSeverity)}} }] }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": {{json (printf "2.%d.4 Notes" (add $i 1))}} }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": {{json $r.Note}} }]
|
||||||
|
},
|
||||||
|
{ "type": "horizontalRule" }{{end}},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 1 },
|
||||||
|
"content": [{ "type": "text", "text": "3. Definitions" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": "Treatment" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Mitigated: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Controls are in place to reduce the likelihood and/or impact of the risk." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Accepted: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The risk is acknowledged and accepted without further action." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Avoided: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Activities giving rise to the risk are avoided altogether." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Transferred: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The risk is shifted to a third party (e.g., insurance, vendor contract)." }] }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": "Likelihood" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "1 — Improbable: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Highly unlikely to occur under normal circumstances." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "2 — Remote: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Unlikely but possible." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "3 — Occasional: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Could occur from time to time." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "4 — Probable: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Likely to occur in most circumstances." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "5 — Frequent: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Expected to occur regularly." }] }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": "Impact" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "1 — Negligible: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Minimal disruption; absorbed by routine operations." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "2 — Low: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Limited disruption; localized impact." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "3 — Moderate: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Notable disruption to operations or finances." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "4 — Significant: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Major disruption; prolonged recovery effort needed." }] }] },
|
||||||
|
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "5 — Catastrophic: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Severe organization-wide impact threatening continuity." }] }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"attrs": { "level": 3 },
|
||||||
|
"content": [{ "type": "text", "text": "Risk Score" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{ "type": "text", "text": "Risk Score is the product of Likelihood × Impact (range 1–25). Inherent values represent the risk before controls; residual values represent the risk after controls have been applied. Severity bands: Low (≤4), High (5–14), Critical (≥15)." }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -239,15 +239,6 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
|||||||
}
|
}
|
||||||
return types.NewTransferImpactAssessment(tia), nil
|
return types.NewTransferImpactAssessment(tia), nil
|
||||||
}
|
}
|
||||||
case coredata.SnapshotEntityType:
|
|
||||||
action = probo.ActionSnapshotList
|
|
||||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
|
||||||
snapshot, err := prb.Snapshots.Get(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return types.NewSnapshot(snapshot), nil
|
|
||||||
}
|
|
||||||
case coredata.TrustCenterEntityType:
|
case coredata.TrustCenterEntityType:
|
||||||
action = probo.ActionTrustCenterGet
|
action = probo.ActionTrustCenterGet
|
||||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||||
|
|||||||
@@ -300,37 +300,6 @@ func (r *controlResolver) Obligations(ctx context.Context, obj *types.Control, f
|
|||||||
return types.NewObligationConnection(page, r, obj.ID), nil
|
return types.NewObligationConnection(page, r, obj.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshots is the resolver for the snapshots field.
|
|
||||||
func (r *controlResolver) Snapshots(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) (*types.SnapshotConnection, error) {
|
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionSnapshotList); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
|
||||||
|
|
||||||
pageOrderBy := page.OrderBy[coredata.SnapshotOrderField]{
|
|
||||||
Field: coredata.SnapshotOrderFieldCreatedAt,
|
|
||||||
Direction: page.OrderDirectionDesc,
|
|
||||||
}
|
|
||||||
|
|
||||||
if orderBy != nil {
|
|
||||||
pageOrderBy = page.OrderBy[coredata.SnapshotOrderField]{
|
|
||||||
Field: orderBy.Field,
|
|
||||||
Direction: orderBy.Direction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
|
||||||
|
|
||||||
page, err := prb.Snapshots.ListForControlID(ctx, obj.ID, cursor)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot list control snapshots", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.NewSnapshotConnection(page, r, obj.ID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission is the resolver for the permission field.
|
// Permission is the resolver for the permission field.
|
||||||
func (r *controlResolver) Permission(ctx context.Context, obj *types.Control, action string) (bool, error) {
|
func (r *controlResolver) Permission(ctx context.Context, obj *types.Control, action string) (bool, error) {
|
||||||
return r.Resolver.Permission(ctx, obj, action)
|
return r.Resolver.Permission(ctx, obj, action)
|
||||||
@@ -708,46 +677,6 @@ func (r *mutationResolver) DeleteControlObligationMapping(ctx context.Context, i
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateControlSnapshotMapping is the resolver for the createControlSnapshotMapping field.
|
|
||||||
func (r *mutationResolver) CreateControlSnapshotMapping(ctx context.Context, input types.CreateControlSnapshotMappingInput) (*types.CreateControlSnapshotMappingPayload, error) {
|
|
||||||
if err := r.authorize(ctx, input.ControlID, probo.ActionControlSnapshotMappingCreate); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.SnapshotID.TenantID())
|
|
||||||
|
|
||||||
control, snapshot, err := prb.Controls.CreateSnapshotMapping(ctx, input.ControlID, input.SnapshotID)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot create control snapshot mapping", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.CreateControlSnapshotMappingPayload{
|
|
||||||
ControlEdge: types.NewControlEdge(control, coredata.ControlOrderFieldCreatedAt),
|
|
||||||
SnapshotEdge: types.NewSnapshotEdge(snapshot, coredata.SnapshotOrderFieldCreatedAt),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteControlSnapshotMapping is the resolver for the deleteControlSnapshotMapping field.
|
|
||||||
func (r *mutationResolver) DeleteControlSnapshotMapping(ctx context.Context, input types.DeleteControlSnapshotMappingInput) (*types.DeleteControlSnapshotMappingPayload, error) {
|
|
||||||
if err := r.authorize(ctx, input.ControlID, probo.ActionControlSnapshotMappingDelete); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.SnapshotID.TenantID())
|
|
||||||
|
|
||||||
control, snapshot, err := prb.Controls.DeleteSnapshotMapping(ctx, input.ControlID, input.SnapshotID)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot delete control snapshot mapping", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.DeleteControlSnapshotMappingPayload{
|
|
||||||
DeletedControlID: control.ID,
|
|
||||||
DeletedSnapshotID: snapshot.ID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateStatementOfApplicability is the resolver for the createStatementOfApplicability field.
|
// CreateStatementOfApplicability is the resolver for the createStatementOfApplicability field.
|
||||||
func (r *mutationResolver) CreateStatementOfApplicability(ctx context.Context, input types.CreateStatementOfApplicabilityInput) (*types.CreateStatementOfApplicabilityPayload, error) {
|
func (r *mutationResolver) CreateStatementOfApplicability(ctx context.Context, input types.CreateStatementOfApplicabilityInput) (*types.CreateStatementOfApplicabilityPayload, error) {
|
||||||
if err := r.authorize(ctx, input.OrganizationID, probo.ActionStatementOfApplicabilityCreate); err != nil {
|
if err := r.authorize(ctx, input.OrganizationID, probo.ActionStatementOfApplicabilityCreate); err != nil {
|
||||||
|
|||||||
@@ -141,14 +141,6 @@ type Control implements Node {
|
|||||||
orderBy: ObligationOrder
|
orderBy: ObligationOrder
|
||||||
): ObligationConnection! @goField(forceResolver: true)
|
): ObligationConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
snapshots(
|
|
||||||
first: Int
|
|
||||||
after: CursorKey
|
|
||||||
last: Int
|
|
||||||
before: CursorKey
|
|
||||||
orderBy: SnapshotOrder
|
|
||||||
): SnapshotConnection! @goField(forceResolver: true)
|
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
|
|
||||||
@@ -265,12 +257,6 @@ extend type Mutation {
|
|||||||
deleteControlObligationMapping(
|
deleteControlObligationMapping(
|
||||||
input: DeleteControlObligationMappingInput!
|
input: DeleteControlObligationMappingInput!
|
||||||
): DeleteControlObligationMappingPayload!
|
): DeleteControlObligationMappingPayload!
|
||||||
createControlSnapshotMapping(
|
|
||||||
input: CreateControlSnapshotMappingInput!
|
|
||||||
): CreateControlSnapshotMappingPayload!
|
|
||||||
deleteControlSnapshotMapping(
|
|
||||||
input: DeleteControlSnapshotMappingInput!
|
|
||||||
): DeleteControlSnapshotMappingPayload!
|
|
||||||
createStatementOfApplicability(
|
createStatementOfApplicability(
|
||||||
input: CreateStatementOfApplicabilityInput!
|
input: CreateStatementOfApplicabilityInput!
|
||||||
): CreateStatementOfApplicabilityPayload!
|
): CreateStatementOfApplicabilityPayload!
|
||||||
@@ -366,16 +352,6 @@ input DeleteControlObligationMappingInput {
|
|||||||
obligationId: ID!
|
obligationId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateControlSnapshotMappingInput {
|
|
||||||
controlId: ID!
|
|
||||||
snapshotId: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
input DeleteControlSnapshotMappingInput {
|
|
||||||
controlId: ID!
|
|
||||||
snapshotId: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
input CreateStatementOfApplicabilityInput {
|
input CreateStatementOfApplicabilityInput {
|
||||||
organizationId: ID!
|
organizationId: ID!
|
||||||
name: String!
|
name: String!
|
||||||
@@ -465,16 +441,6 @@ type DeleteControlObligationMappingPayload {
|
|||||||
deletedObligationId: ID!
|
deletedObligationId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateControlSnapshotMappingPayload {
|
|
||||||
controlEdge: ControlEdge!
|
|
||||||
snapshotEdge: SnapshotEdge!
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteControlSnapshotMappingPayload {
|
|
||||||
deletedControlId: ID!
|
|
||||||
deletedSnapshotId: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateStatementOfApplicabilityPayload {
|
type CreateStatementOfApplicabilityPayload {
|
||||||
statementOfApplicabilityEdge: StatementOfApplicabilityEdge!
|
statementOfApplicabilityEdge: StatementOfApplicabilityEdge!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,16 +286,10 @@ type Organization implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
orderBy: RiskOrder
|
orderBy: RiskOrder
|
||||||
filter: RiskFilter = { snapshotId: null }
|
filter: RiskFilter
|
||||||
): RiskConnection! @goField(forceResolver: true)
|
): RiskConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
snapshots(
|
risksDocument: Document @goField(forceResolver: true)
|
||||||
first: Int
|
|
||||||
after: CursorKey
|
|
||||||
last: Int
|
|
||||||
before: CursorKey
|
|
||||||
orderBy: SnapshotOrder
|
|
||||||
): SnapshotConnection! @goField(forceResolver: true)
|
|
||||||
|
|
||||||
tasks(
|
tasks(
|
||||||
first: Int
|
first: Int
|
||||||
|
|||||||
@@ -53,12 +53,10 @@ input RiskOrder
|
|||||||
|
|
||||||
input RiskFilter {
|
input RiskFilter {
|
||||||
query: String
|
query: String
|
||||||
snapshotId: ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Risk implements Node {
|
type Risk implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
snapshotId: ID
|
|
||||||
name: String!
|
name: String!
|
||||||
description: String
|
description: String
|
||||||
category: String!
|
category: String!
|
||||||
@@ -151,6 +149,19 @@ extend type Mutation {
|
|||||||
deleteRiskObligationMapping(
|
deleteRiskObligationMapping(
|
||||||
input: DeleteRiskObligationMappingInput!
|
input: DeleteRiskObligationMappingInput!
|
||||||
): DeleteRiskObligationMappingPayload!
|
): DeleteRiskObligationMappingPayload!
|
||||||
|
publishRiskList(
|
||||||
|
input: PublishRiskListInput!
|
||||||
|
): PublishRiskListPayload!
|
||||||
|
}
|
||||||
|
|
||||||
|
input PublishRiskListInput {
|
||||||
|
organizationId: ID!
|
||||||
|
approverIds: [ID!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishRiskListPayload {
|
||||||
|
documentEdge: DocumentEdge!
|
||||||
|
documentVersionEdge: DocumentVersionEdge!
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateRiskInput {
|
input CreateRiskInput {
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
enum SnapshotsType
|
|
||||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SnapshotsType") {
|
|
||||||
RISKS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SnapshotOrderField
|
|
||||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SnapshotOrderField") {
|
|
||||||
CREATED_AT
|
|
||||||
@goEnum(
|
|
||||||
value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldCreatedAt"
|
|
||||||
)
|
|
||||||
NAME
|
|
||||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldName")
|
|
||||||
TYPE
|
|
||||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldType")
|
|
||||||
}
|
|
||||||
|
|
||||||
input SnapshotOrder
|
|
||||||
@goModel(
|
|
||||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.SnapshotOrderBy"
|
|
||||||
) {
|
|
||||||
direction: OrderDirection!
|
|
||||||
field: SnapshotOrderField!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Snapshot implements Node {
|
|
||||||
id: ID!
|
|
||||||
organization: Organization! @goField(forceResolver: true)
|
|
||||||
name: String!
|
|
||||||
description: String
|
|
||||||
type: SnapshotsType!
|
|
||||||
|
|
||||||
controls(
|
|
||||||
first: Int
|
|
||||||
after: CursorKey
|
|
||||||
last: Int
|
|
||||||
before: CursorKey
|
|
||||||
orderBy: ControlOrder
|
|
||||||
filter: ControlFilter
|
|
||||||
): ControlConnection! @goField(forceResolver: true)
|
|
||||||
|
|
||||||
createdAt: Datetime!
|
|
||||||
|
|
||||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshotConnection
|
|
||||||
@goModel(
|
|
||||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.SnapshotConnection"
|
|
||||||
) {
|
|
||||||
totalCount: Int! @goField(forceResolver: true)
|
|
||||||
edges: [SnapshotEdge!]!
|
|
||||||
pageInfo: PageInfo!
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnapshotEdge {
|
|
||||||
cursor: CursorKey!
|
|
||||||
node: Snapshot!
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
createSnapshot(input: CreateSnapshotInput!): CreateSnapshotPayload!
|
|
||||||
deleteSnapshot(input: DeleteSnapshotInput!): DeleteSnapshotPayload!
|
|
||||||
}
|
|
||||||
|
|
||||||
input CreateSnapshotInput {
|
|
||||||
organizationId: ID!
|
|
||||||
name: String!
|
|
||||||
description: String
|
|
||||||
type: SnapshotsType!
|
|
||||||
}
|
|
||||||
|
|
||||||
input DeleteSnapshotInput {
|
|
||||||
snapshotId: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateSnapshotPayload {
|
|
||||||
snapshotEdge: SnapshotEdge!
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteSnapshotPayload {
|
|
||||||
deletedSnapshotId: ID!
|
|
||||||
}
|
|
||||||
@@ -101,9 +101,9 @@ func (r *measureResolver) Risks(ctx context.Context, obj *types.Measure, first *
|
|||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
var riskFilter = coredata.NewRiskFilter(nil, nil)
|
var riskFilter = coredata.NewRiskFilter(nil)
|
||||||
if filter != nil {
|
if filter != nil {
|
||||||
riskFilter = coredata.NewRiskFilter(filter.Query, &filter.SnapshotID)
|
riskFilter = coredata.NewRiskFilter(filter.Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
page, err := prb.Risks.ListForMeasureID(ctx, obj.ID, cursor, riskFilter)
|
page, err := prb.Risks.ListForMeasureID(ctx, obj.ID, cursor, riskFilter)
|
||||||
|
|||||||
@@ -1027,9 +1027,9 @@ func (r *organizationResolver) Risks(ctx context.Context, obj *types.Organizatio
|
|||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
var riskFilter = coredata.NewRiskFilter(nil, nil)
|
var riskFilter = coredata.NewRiskFilter(nil)
|
||||||
if filter != nil {
|
if filter != nil {
|
||||||
riskFilter = coredata.NewRiskFilter(filter.Query, &filter.SnapshotID)
|
riskFilter = coredata.NewRiskFilter(filter.Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
page, err := prb.Risks.ListForOrganizationID(ctx, obj.ID, cursor, riskFilter)
|
page, err := prb.Risks.ListForOrganizationID(ctx, obj.ID, cursor, riskFilter)
|
||||||
@@ -1041,34 +1041,33 @@ func (r *organizationResolver) Risks(ctx context.Context, obj *types.Organizatio
|
|||||||
return types.NewRiskConnection(page, r, obj.ID, riskFilter), nil
|
return types.NewRiskConnection(page, r, obj.ID, riskFilter), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshots is the resolver for the snapshots field.
|
// RisksDocument is the resolver for the risksDocument field.
|
||||||
func (r *organizationResolver) Snapshots(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) (*types.SnapshotConnection, error) {
|
func (r *organizationResolver) RisksDocument(ctx context.Context, obj *types.Organization) (*types.Document, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionSnapshotList); err != nil {
|
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||||
|
|
||||||
pageOrderBy := page.OrderBy[coredata.SnapshotOrderField]{
|
documentID, err := prb.GeneratedDocuments.GetRisksDocumentID(ctx, obj.ID)
|
||||||
Field: coredata.SnapshotOrderFieldCreatedAt,
|
|
||||||
Direction: page.OrderDirectionDesc,
|
|
||||||
}
|
|
||||||
if orderBy != nil {
|
|
||||||
pageOrderBy = page.OrderBy[coredata.SnapshotOrderField]{
|
|
||||||
Field: orderBy.Field,
|
|
||||||
Direction: orderBy.Direction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
|
||||||
|
|
||||||
page, err := prb.Snapshots.ListForOrganizationID(ctx, obj.ID, cursor)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.ErrorCtx(ctx, "cannot list organization snapshots", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot get risks document ID", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
if documentID == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
document, err := prb.Documents.Get(ctx, *documentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot load risks document", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewSnapshotConnection(page, r, obj.ID), nil
|
return types.NewDocument(document), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tasks is the resolver for the tasks field.
|
// Tasks is the resolver for the tasks field.
|
||||||
|
|||||||
@@ -239,6 +239,29 @@ func (r *mutationResolver) DeleteRiskObligationMapping(ctx context.Context, inpu
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishRiskList is the resolver for the publishRiskList field.
|
||||||
|
func (r *mutationResolver) PublishRiskList(ctx context.Context, input types.PublishRiskListInput) (*types.PublishRiskListPayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.OrganizationID, probo.ActionRiskPublish); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||||
|
|
||||||
|
document, documentVersion, err := prb.GeneratedDocuments.PublishRiskList(ctx, input.OrganizationID, input.ApproverIds)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||||
|
return nil, gqlutils.Conflict(ctx, err)
|
||||||
|
}
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot publish risk list", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.PublishRiskListPayload{
|
||||||
|
DocumentEdge: types.NewDocumentEdge(document, coredata.DocumentOrderFieldCreatedAt),
|
||||||
|
DocumentVersionEdge: types.NewDocumentVersionEdge(documentVersion, coredata.DocumentVersionOrderFieldCreatedAt),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Owner is the resolver for the owner field.
|
// Owner is the resolver for the owner field.
|
||||||
func (r *riskResolver) Owner(ctx context.Context, obj *types.Risk) (*types.Profile, error) {
|
func (r *riskResolver) Owner(ctx context.Context, obj *types.Risk) (*types.Profile, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil {
|
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileGet); err != nil {
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
package console_v1
|
|
||||||
|
|
||||||
// This file will be automatically regenerated based on the schema, any resolver
|
|
||||||
// implementations
|
|
||||||
// will be copied through when generating and any unknown code will be moved to the end.
|
|
||||||
// Code generated by github.com/99designs/gqlgen version v0.17.87
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"go.gearno.de/kit/log"
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"go.probo.inc/probo/pkg/probo"
|
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
|
||||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateSnapshot is the resolver for the createSnapshot field.
|
|
||||||
func (r *mutationResolver) CreateSnapshot(ctx context.Context, input types.CreateSnapshotInput) (*types.CreateSnapshotPayload, error) {
|
|
||||||
if err := r.authorize(ctx, input.OrganizationID, probo.ActionSnapshotCreate); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
|
||||||
|
|
||||||
snapshot, err := prb.Snapshots.Create(
|
|
||||||
ctx,
|
|
||||||
&probo.CreateSnapshotRequest{
|
|
||||||
OrganizationID: input.OrganizationID,
|
|
||||||
Name: input.Name,
|
|
||||||
Description: input.Description,
|
|
||||||
Type: input.Type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot create snapshot", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.CreateSnapshotPayload{
|
|
||||||
SnapshotEdge: types.NewSnapshotEdge(snapshot, coredata.SnapshotOrderFieldCreatedAt),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSnapshot is the resolver for the deleteSnapshot field.
|
|
||||||
func (r *mutationResolver) DeleteSnapshot(ctx context.Context, input types.DeleteSnapshotInput) (*types.DeleteSnapshotPayload, error) {
|
|
||||||
if err := r.authorize(ctx, input.SnapshotID, probo.ActionSnapshotDelete); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.SnapshotID.TenantID())
|
|
||||||
|
|
||||||
err := prb.Snapshots.Delete(ctx, input.SnapshotID)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot delete snapshot", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.DeleteSnapshotPayload{
|
|
||||||
DeletedSnapshotID: input.SnapshotID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Organization is the resolver for the organization field.
|
|
||||||
func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error) {
|
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
|
||||||
|
|
||||||
snapshot, err := prb.Snapshots.Get(ctx, obj.ID)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot get snapshot", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
organization, err := prb.Organizations.Get(ctx, snapshot.OrganizationID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
||||||
return nil, gqlutils.NotFound(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.NewOrganization(organization), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controls is the resolver for the controls field.
|
|
||||||
func (r *snapshotResolver) Controls(ctx context.Context, obj *types.Snapshot, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) {
|
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionControlList); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
|
||||||
|
|
||||||
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
|
|
||||||
Field: coredata.ControlOrderFieldCreatedAt,
|
|
||||||
Direction: page.OrderDirectionDesc,
|
|
||||||
}
|
|
||||||
if orderBy != nil {
|
|
||||||
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
|
|
||||||
Field: orderBy.Field,
|
|
||||||
Direction: orderBy.Direction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
|
||||||
|
|
||||||
var controlFilter = coredata.NewControlFilter(nil)
|
|
||||||
if filter != nil {
|
|
||||||
controlFilter = coredata.NewControlFilter(filter.Query)
|
|
||||||
}
|
|
||||||
|
|
||||||
page, err := prb.Controls.ListForSnapshotID(ctx, obj.ID, cursor, controlFilter)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot list snapshot controls", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.NewControlConnection(page, r, obj.ID, controlFilter), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission is the resolver for the permission field.
|
|
||||||
func (r *snapshotResolver) Permission(ctx context.Context, obj *types.Snapshot, action string) (bool, error) {
|
|
||||||
return r.Resolver.Permission(ctx, obj, action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TotalCount is the resolver for the totalCount field.
|
|
||||||
func (r *snapshotConnectionResolver) TotalCount(ctx context.Context, obj *types.SnapshotConnection) (int, error) {
|
|
||||||
if err := r.authorize(ctx, obj.ParentID, probo.ActionSnapshotList); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, obj.ParentID.TenantID())
|
|
||||||
|
|
||||||
switch obj.Resolver.(type) {
|
|
||||||
case *organizationResolver:
|
|
||||||
count, err := prb.Snapshots.CountForOrganizationID(ctx, obj.ParentID)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot count snapshots", log.Error(err))
|
|
||||||
return 0, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "unsupported resolver")
|
|
||||||
return 0, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot returns schema.SnapshotResolver implementation.
|
|
||||||
func (r *Resolver) Snapshot() schema.SnapshotResolver { return &snapshotResolver{r} }
|
|
||||||
|
|
||||||
// SnapshotConnection returns schema.SnapshotConnectionResolver implementation.
|
|
||||||
func (r *Resolver) SnapshotConnection() schema.SnapshotConnectionResolver {
|
|
||||||
return &snapshotConnectionResolver{r}
|
|
||||||
}
|
|
||||||
|
|
||||||
type snapshotResolver struct{ *Resolver }
|
|
||||||
type snapshotConnectionResolver struct{ *Resolver }
|
|
||||||
@@ -67,7 +67,6 @@ func NewRisk(r *coredata.Risk) *Risk {
|
|||||||
risk := &Risk{
|
risk := &Risk{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
SnapshotID: r.SnapshotID,
|
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
Treatment: r.Treatment,
|
Treatment: r.Treatment,
|
||||||
InherentLikelihood: r.InherentLikelihood,
|
InherentLikelihood: r.InherentLikelihood,
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
SnapshotOrderBy OrderBy[coredata.SnapshotOrderField]
|
|
||||||
|
|
||||||
SnapshotConnection struct {
|
|
||||||
TotalCount int
|
|
||||||
Edges []*SnapshotEdge
|
|
||||||
PageInfo PageInfo
|
|
||||||
|
|
||||||
Resolver any
|
|
||||||
ParentID gid.GID
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewSnapshotConnection(
|
|
||||||
p *page.Page[*coredata.Snapshot, coredata.SnapshotOrderField],
|
|
||||||
parentType any,
|
|
||||||
parentID gid.GID,
|
|
||||||
) *SnapshotConnection {
|
|
||||||
edges := make([]*SnapshotEdge, len(p.Data))
|
|
||||||
for i, snapshot := range p.Data {
|
|
||||||
edges[i] = NewSnapshotEdge(snapshot, p.Cursor.OrderBy.Field)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SnapshotConnection{
|
|
||||||
Edges: edges,
|
|
||||||
PageInfo: *NewPageInfo(p),
|
|
||||||
|
|
||||||
Resolver: parentType,
|
|
||||||
ParentID: parentID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSnapshotEdge(s *coredata.Snapshot, orderField coredata.SnapshotOrderField) *SnapshotEdge {
|
|
||||||
return &SnapshotEdge{
|
|
||||||
Node: NewSnapshot(s),
|
|
||||||
Cursor: s.CursorKey(orderField),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSnapshot(s *coredata.Snapshot) *Snapshot {
|
|
||||||
return &Snapshot{
|
|
||||||
ID: s.ID,
|
|
||||||
Organization: &Organization{
|
|
||||||
ID: s.OrganizationID,
|
|
||||||
},
|
|
||||||
Name: s.Name,
|
|
||||||
Type: s.Type,
|
|
||||||
Description: s.Description,
|
|
||||||
CreatedAt: s.CreatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -274,10 +274,9 @@ func (r *Resolver) ListRisksTool(ctx context.Context, req *mcp.CallToolRequest,
|
|||||||
|
|
||||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||||
|
|
||||||
noSnapshot := (*gid.GID)(nil)
|
riskFilter := coredata.NewRiskFilter(nil)
|
||||||
riskFilter := coredata.NewRiskFilter(nil, &noSnapshot)
|
|
||||||
if input.Filter != nil {
|
if input.Filter != nil {
|
||||||
riskFilter = coredata.NewRiskFilter(input.Filter.Query, &input.Filter.SnapshotID)
|
riskFilter = coredata.NewRiskFilter(input.Filter.Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
page, err := prb.Risks.ListForOrganizationID(ctx, input.OrganizationID, cursor, riskFilter)
|
page, err := prb.Risks.ListForOrganizationID(ctx, input.OrganizationID, cursor, riskFilter)
|
||||||
@@ -1511,11 +1510,6 @@ func (r *Resolver) LinkControlTool(ctx context.Context, req *mcp.CallToolRequest
|
|||||||
if _, _, err := svc.Controls.CreateAuditMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
if _, _, err := svc.Controls.CreateAuditMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
||||||
return nil, types.LinkControlOutput{}, fmt.Errorf("failed to link control to audit: %w", err)
|
return nil, types.LinkControlOutput{}, fmt.Errorf("failed to link control to audit: %w", err)
|
||||||
}
|
}
|
||||||
case coredata.SnapshotEntityType:
|
|
||||||
r.MustAuthorize(ctx, input.ControlID, probo.ActionControlSnapshotMappingCreate)
|
|
||||||
if _, _, err := svc.Controls.CreateSnapshotMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
|
||||||
return nil, types.LinkControlOutput{}, fmt.Errorf("failed to link control to snapshot: %w", err)
|
|
||||||
}
|
|
||||||
case coredata.ObligationEntityType:
|
case coredata.ObligationEntityType:
|
||||||
r.MustAuthorize(ctx, input.ControlID, probo.ActionControlObligationMappingCreate)
|
r.MustAuthorize(ctx, input.ControlID, probo.ActionControlObligationMappingCreate)
|
||||||
if _, _, err := svc.Controls.CreateObligationMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
if _, _, err := svc.Controls.CreateObligationMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
||||||
@@ -1547,11 +1541,6 @@ func (r *Resolver) UnlinkControlTool(ctx context.Context, req *mcp.CallToolReque
|
|||||||
if _, _, err := svc.Controls.DeleteAuditMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
if _, _, err := svc.Controls.DeleteAuditMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
||||||
return nil, types.UnlinkControlOutput{}, fmt.Errorf("failed to unlink control from audit: %w", err)
|
return nil, types.UnlinkControlOutput{}, fmt.Errorf("failed to unlink control from audit: %w", err)
|
||||||
}
|
}
|
||||||
case coredata.SnapshotEntityType:
|
|
||||||
r.MustAuthorize(ctx, input.ControlID, probo.ActionControlSnapshotMappingDelete)
|
|
||||||
if _, _, err := svc.Controls.DeleteSnapshotMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
|
||||||
return nil, types.UnlinkControlOutput{}, fmt.Errorf("failed to unlink control from snapshot: %w", err)
|
|
||||||
}
|
|
||||||
case coredata.ObligationEntityType:
|
case coredata.ObligationEntityType:
|
||||||
r.MustAuthorize(ctx, input.ControlID, probo.ActionControlObligationMappingDelete)
|
r.MustAuthorize(ctx, input.ControlID, probo.ActionControlObligationMappingDelete)
|
||||||
if _, _, err := svc.Controls.DeleteObligationMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
if _, _, err := svc.Controls.DeleteObligationMapping(ctx, input.ControlID, input.ResourceID); err != nil {
|
||||||
@@ -1668,32 +1657,6 @@ func (r *Resolver) ListControlAuditsTool(ctx context.Context, req *mcp.CallToolR
|
|||||||
return nil, types.NewListControlAuditsOutput(auditPage), nil
|
return nil, types.NewListControlAuditsOutput(auditPage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) ListControlSnapshotsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListControlSnapshotsInput) (*mcp.CallToolResult, types.ListControlSnapshotsOutput, error) {
|
|
||||||
r.MustAuthorize(ctx, input.ControlID, probo.ActionControlGet)
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.ControlID)
|
|
||||||
|
|
||||||
pageOrderBy := page.OrderBy[coredata.SnapshotOrderField]{
|
|
||||||
Field: coredata.SnapshotOrderFieldCreatedAt,
|
|
||||||
Direction: page.OrderDirectionDesc,
|
|
||||||
}
|
|
||||||
if input.OrderBy != nil {
|
|
||||||
pageOrderBy = page.OrderBy[coredata.SnapshotOrderField]{
|
|
||||||
Field: input.OrderBy.Field,
|
|
||||||
Direction: input.OrderBy.Direction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
|
||||||
|
|
||||||
snapshotPage, err := prb.Snapshots.ListForControlID(ctx, input.ControlID, cursor)
|
|
||||||
if err != nil {
|
|
||||||
return nil, types.ListControlSnapshotsOutput{}, fmt.Errorf("failed to list control snapshots: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, types.NewListControlSnapshotsOutput(snapshotPage), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) ListRiskObligationsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskObligationsInput) (*mcp.CallToolResult, types.ListRiskObligationsOutput, error) {
|
func (r *Resolver) ListRiskObligationsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskObligationsInput) (*mcp.CallToolResult, types.ListRiskObligationsOutput, error) {
|
||||||
r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskGet)
|
r.MustAuthorize(ctx, input.RiskID, probo.ActionRiskGet)
|
||||||
|
|
||||||
@@ -1915,68 +1878,6 @@ func (r *Resolver) DeleteTaskTool(ctx context.Context, req *mcp.CallToolRequest,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) ListSnapshotsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListSnapshotsInput) (*mcp.CallToolResult, types.ListSnapshotsOutput, error) {
|
|
||||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionSnapshotList)
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.OrganizationID)
|
|
||||||
|
|
||||||
pageOrderBy := page.OrderBy[coredata.SnapshotOrderField]{
|
|
||||||
Field: coredata.SnapshotOrderFieldCreatedAt,
|
|
||||||
Direction: page.OrderDirectionDesc,
|
|
||||||
}
|
|
||||||
if input.OrderBy != nil {
|
|
||||||
pageOrderBy = page.OrderBy[coredata.SnapshotOrderField]{
|
|
||||||
Field: input.OrderBy.Field,
|
|
||||||
Direction: input.OrderBy.Direction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
|
||||||
|
|
||||||
page, err := prb.Snapshots.ListForOrganizationID(ctx, input.OrganizationID, cursor)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("cannot list organization snapshots: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, types.NewListSnapshotsOutput(page), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) GetSnapshotTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetSnapshotInput) (*mcp.CallToolResult, types.GetSnapshotOutput, error) {
|
|
||||||
r.MustAuthorize(ctx, input.ID, probo.ActionSnapshotGet)
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.ID)
|
|
||||||
|
|
||||||
snapshot, err := prb.Snapshots.Get(ctx, input.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, types.GetSnapshotOutput{}, fmt.Errorf("failed to get snapshot: %w", err)
|
|
||||||
}
|
|
||||||
return nil, types.GetSnapshotOutput{
|
|
||||||
Snapshot: types.NewSnapshot(snapshot),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) TakeSnapshotTool(ctx context.Context, req *mcp.CallToolRequest, input *types.TakeSnapshotInput) (*mcp.CallToolResult, types.TakeSnapshotOutput, error) {
|
|
||||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionSnapshotCreate)
|
|
||||||
|
|
||||||
prb := r.ProboService(ctx, input.OrganizationID)
|
|
||||||
|
|
||||||
snapshot, err := prb.Snapshots.Create(
|
|
||||||
ctx,
|
|
||||||
&probo.CreateSnapshotRequest{
|
|
||||||
OrganizationID: input.OrganizationID,
|
|
||||||
Name: input.Name,
|
|
||||||
Description: input.Description,
|
|
||||||
Type: input.Type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, types.TakeSnapshotOutput{}, fmt.Errorf("failed to take snapshot: %w", err)
|
|
||||||
}
|
|
||||||
return nil, types.TakeSnapshotOutput{
|
|
||||||
Snapshot: types.NewSnapshot(snapshot),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) ListDocumentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentsInput) (*mcp.CallToolResult, types.ListDocumentsOutput, error) {
|
func (r *Resolver) ListDocumentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentsInput) (*mcp.CallToolResult, types.ListDocumentsOutput, error) {
|
||||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionDocumentList)
|
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionDocumentList)
|
||||||
|
|
||||||
@@ -2314,7 +2215,7 @@ func (r *Resolver) ListMeasureRisksTool(ctx context.Context, req *mcp.CallToolRe
|
|||||||
|
|
||||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||||
|
|
||||||
riskPage, err := prb.Risks.ListForMeasureID(ctx, input.MeasureID, cursor, coredata.NewRiskFilter(nil, nil))
|
riskPage, err := prb.Risks.ListForMeasureID(ctx, input.MeasureID, cursor, coredata.NewRiskFilter(nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.ListMeasureRisksOutput{}, fmt.Errorf("failed to list measure risks: %w", err)
|
return nil, types.ListMeasureRisksOutput{}, fmt.Errorf("failed to list measure risks: %w", err)
|
||||||
}
|
}
|
||||||
@@ -5190,3 +5091,19 @@ func (r *Resolver) GetCookieConsentRecordTool(ctx context.Context, req *mcp.Call
|
|||||||
}
|
}
|
||||||
return nil, types.GetCookieConsentRecordOutput{CookieConsentRecord: types.NewCookieConsentRecord(record)}, nil
|
return nil, types.GetCookieConsentRecordOutput{CookieConsentRecord: types.NewCookieConsentRecord(record)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) PublishRiskListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishRiskListInput) (*mcp.CallToolResult, types.PublishRiskListOutput, error) {
|
||||||
|
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionRiskPublish)
|
||||||
|
|
||||||
|
svc := r.ProboService(ctx, input.OrganizationID)
|
||||||
|
|
||||||
|
document, documentVersion, err := svc.GeneratedDocuments.PublishRiskList(ctx, input.OrganizationID, input.ApproverIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.PublishRiskListOutput{}, fmt.Errorf("cannot publish risk list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.PublishRiskListOutput{
|
||||||
|
DocumentID: document.ID,
|
||||||
|
DocumentVersionID: documentVersion.ID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1499,13 +1499,6 @@ components:
|
|||||||
organization_id:
|
organization_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Organization ID
|
description: Organization ID
|
||||||
snapshot_id:
|
|
||||||
anyOf:
|
|
||||||
- type: string
|
|
||||||
$ref: "#/components/schemas/GID"
|
|
||||||
- type: "null"
|
|
||||||
description: No snapshot
|
|
||||||
description: Snapshot ID
|
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: Risk name
|
description: Risk name
|
||||||
@@ -1579,12 +1572,6 @@ components:
|
|||||||
query:
|
query:
|
||||||
type: string
|
type: string
|
||||||
description: Search query
|
description: Search query
|
||||||
snapshot_id:
|
|
||||||
anyOf:
|
|
||||||
- $ref: "#/components/schemas/GID"
|
|
||||||
- type: "null"
|
|
||||||
description: Filter by snapshot ID. Defaults to null, which returns only risks with no snapshot (current live data). Pass a specific snapshot ID to retrieve risks as they were at that snapshot.
|
|
||||||
default: null
|
|
||||||
|
|
||||||
ListRisksOutput:
|
ListRisksOutput:
|
||||||
type: object
|
type: object
|
||||||
@@ -4772,7 +4759,7 @@ components:
|
|||||||
description: Control ID
|
description: Control ID
|
||||||
resource_id:
|
resource_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: ID of the resource to link (measure, document, audit, snapshot, or obligation)
|
description: ID of the resource to link (measure, document, audit, or obligation)
|
||||||
|
|
||||||
LinkControlOutput:
|
LinkControlOutput:
|
||||||
type: object
|
type: object
|
||||||
@@ -4788,7 +4775,7 @@ components:
|
|||||||
description: Control ID
|
description: Control ID
|
||||||
resource_id:
|
resource_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: ID of the resource to unlink (measure, document, audit, snapshot, or obligation)
|
description: ID of the resource to unlink (measure, document, audit, or obligation)
|
||||||
|
|
||||||
UnlinkControlOutput:
|
UnlinkControlOutput:
|
||||||
type: object
|
type: object
|
||||||
@@ -4917,37 +4904,6 @@ components:
|
|||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Audit"
|
$ref: "#/components/schemas/Audit"
|
||||||
|
|
||||||
ListControlSnapshotsInput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- control_id
|
|
||||||
properties:
|
|
||||||
control_id:
|
|
||||||
$ref: "#/components/schemas/GID"
|
|
||||||
description: Control ID
|
|
||||||
cursor:
|
|
||||||
$ref: "#/components/schemas/CursorKey"
|
|
||||||
description: Page cursor
|
|
||||||
size:
|
|
||||||
type: integer
|
|
||||||
description: Page size
|
|
||||||
order_by:
|
|
||||||
$ref: "#/components/schemas/SnapshotOrderBy"
|
|
||||||
description: Snapshot order by
|
|
||||||
|
|
||||||
ListControlSnapshotsOutput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- snapshots
|
|
||||||
properties:
|
|
||||||
next_cursor:
|
|
||||||
$ref: "#/components/schemas/CursorKey"
|
|
||||||
description: Next cursor
|
|
||||||
snapshots:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/Snapshot"
|
|
||||||
|
|
||||||
ListRiskObligationsInput:
|
ListRiskObligationsInput:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -5354,150 +5310,6 @@ components:
|
|||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Deleted task ID
|
description: Deleted task ID
|
||||||
|
|
||||||
SnapshotsType:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- RISKS
|
|
||||||
- NONCONFORMITIES
|
|
||||||
- OBLIGATIONS
|
|
||||||
- CONTINUAL_IMPROVEMENTS
|
|
||||||
- PROCESSING_ACTIVITIES
|
|
||||||
- STATEMENTS_OF_APPLICABILITY
|
|
||||||
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SnapshotsType
|
|
||||||
|
|
||||||
SnapshotOrderField:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- CREATED_AT
|
|
||||||
- NAME
|
|
||||||
- TYPE
|
|
||||||
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SnapshotOrderField
|
|
||||||
|
|
||||||
SnapshotOrderBy:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- field
|
|
||||||
- direction
|
|
||||||
properties:
|
|
||||||
field:
|
|
||||||
$ref: "#/components/schemas/SnapshotOrderField"
|
|
||||||
description: Snapshot order field
|
|
||||||
direction:
|
|
||||||
$ref: "#/components/schemas/OrderDirection"
|
|
||||||
description: Snapshot order direction
|
|
||||||
|
|
||||||
Snapshot:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- organization_id
|
|
||||||
- name
|
|
||||||
- type
|
|
||||||
- created_at
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
$ref: "#/components/schemas/GID"
|
|
||||||
description: Snapshot ID
|
|
||||||
organization_id:
|
|
||||||
$ref: "#/components/schemas/GID"
|
|
||||||
description: Organization ID
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description: Snapshot name
|
|
||||||
description:
|
|
||||||
anyOf:
|
|
||||||
- type: string
|
|
||||||
description: Snapshot description
|
|
||||||
- type: "null"
|
|
||||||
description: No description
|
|
||||||
description: Snapshot description
|
|
||||||
type:
|
|
||||||
$ref: "#/components/schemas/SnapshotsType"
|
|
||||||
description: Snapshot type
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
description: Creation timestamp
|
|
||||||
|
|
||||||
ListSnapshotsInput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- organization_id
|
|
||||||
properties:
|
|
||||||
organization_id:
|
|
||||||
$ref: "#/components/schemas/GID"
|
|
||||||
description: Organization ID
|
|
||||||
order_by:
|
|
||||||
$ref: "#/components/schemas/SnapshotOrderBy"
|
|
||||||
description: Snapshot order by
|
|
||||||
size:
|
|
||||||
type: integer
|
|
||||||
description: Page size
|
|
||||||
cursor:
|
|
||||||
$ref: "#/components/schemas/CursorKey"
|
|
||||||
description: Page cursor
|
|
||||||
|
|
||||||
ListSnapshotsOutput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- snapshots
|
|
||||||
properties:
|
|
||||||
snapshots:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/Snapshot"
|
|
||||||
description: List of snapshots
|
|
||||||
next_cursor:
|
|
||||||
anyOf:
|
|
||||||
- $ref: "#/components/schemas/CursorKey"
|
|
||||||
- type: "null"
|
|
||||||
description: Next page cursor
|
|
||||||
|
|
||||||
GetSnapshotInput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
$ref: "#/components/schemas/GID"
|
|
||||||
description: Snapshot ID
|
|
||||||
|
|
||||||
GetSnapshotOutput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- snapshot
|
|
||||||
properties:
|
|
||||||
snapshot:
|
|
||||||
$ref: "#/components/schemas/Snapshot"
|
|
||||||
|
|
||||||
TakeSnapshotInput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- organization_id
|
|
||||||
- name
|
|
||||||
- type
|
|
||||||
properties:
|
|
||||||
organization_id:
|
|
||||||
$ref: "#/components/schemas/GID"
|
|
||||||
description: Organization ID
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description: Snapshot name
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
description: Snapshot description
|
|
||||||
type:
|
|
||||||
$ref: "#/components/schemas/SnapshotsType"
|
|
||||||
description: Snapshot type (determines which collection to snapshot)
|
|
||||||
|
|
||||||
TakeSnapshotOutput:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- snapshot
|
|
||||||
properties:
|
|
||||||
snapshot:
|
|
||||||
$ref: "#/components/schemas/Snapshot"
|
|
||||||
|
|
||||||
DocumentType:
|
DocumentType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -6815,16 +6627,6 @@ components:
|
|||||||
cursor:
|
cursor:
|
||||||
$ref: "#/components/schemas/CursorKey"
|
$ref: "#/components/schemas/CursorKey"
|
||||||
description: Page cursor
|
description: Page cursor
|
||||||
filter:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
snapshot_id:
|
|
||||||
anyOf:
|
|
||||||
- $ref: "#/components/schemas/GID"
|
|
||||||
- type: "null"
|
|
||||||
description: Filter by snapshot ID. Defaults to null, which returns only statements of applicability with no snapshot (current live data). Pass a specific snapshot ID to retrieve statements of applicability as they were at that snapshot.
|
|
||||||
default: null
|
|
||||||
|
|
||||||
ListStatementsOfApplicabilityOutput:
|
ListStatementsOfApplicabilityOutput:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -7131,6 +6933,33 @@ components:
|
|||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Created document version ID
|
description: Created document version ID
|
||||||
|
|
||||||
|
PublishRiskListInput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- organization_id
|
||||||
|
properties:
|
||||||
|
organization_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Organization ID
|
||||||
|
approver_ids:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Optional approver profile IDs. If provided, creates a draft pending approval instead of publishing immediately.
|
||||||
|
|
||||||
|
PublishRiskListOutput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- document_id
|
||||||
|
- document_version_id
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Created or updated document ID
|
||||||
|
document_version_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Created document version ID
|
||||||
|
|
||||||
PublishStatementOfApplicabilityInput:
|
PublishStatementOfApplicabilityInput:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -7201,11 +7030,6 @@ components:
|
|||||||
organization_id:
|
organization_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Organization ID
|
description: Organization ID
|
||||||
snapshot_id:
|
|
||||||
anyOf:
|
|
||||||
- $ref: "#/components/schemas/GID"
|
|
||||||
- type: "null"
|
|
||||||
description: Snapshot ID
|
|
||||||
applicability:
|
applicability:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Whether the control is applicable
|
description: Whether the control is applicable
|
||||||
@@ -10842,7 +10666,7 @@ tools:
|
|||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/UpdateControlOutput"
|
$ref: "#/components/schemas/UpdateControlOutput"
|
||||||
- name: linkControl
|
- name: linkControl
|
||||||
description: Link a resource to a control (measure, document, audit, snapshot, or obligation). The resource type is determined from the resource_id GID.
|
description: Link a resource to a control (measure, document, audit, or obligation). The resource type is determined from the resource_id GID.
|
||||||
hints:
|
hints:
|
||||||
readonly: false
|
readonly: false
|
||||||
inputSchema:
|
inputSchema:
|
||||||
@@ -10850,7 +10674,7 @@ tools:
|
|||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/LinkControlOutput"
|
$ref: "#/components/schemas/LinkControlOutput"
|
||||||
- name: unlinkControl
|
- name: unlinkControl
|
||||||
description: Unlink a resource from a control (measure, document, audit, snapshot, or obligation). The resource type is determined from the resource_id GID.
|
description: Unlink a resource from a control (measure, document, audit, or obligation). The resource type is determined from the resource_id GID.
|
||||||
hints:
|
hints:
|
||||||
readonly: false
|
readonly: false
|
||||||
inputSchema:
|
inputSchema:
|
||||||
@@ -10893,15 +10717,6 @@ tools:
|
|||||||
$ref: "#/components/schemas/ListControlAuditsInput"
|
$ref: "#/components/schemas/ListControlAuditsInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/ListControlAuditsOutput"
|
$ref: "#/components/schemas/ListControlAuditsOutput"
|
||||||
- name: listControlSnapshots
|
|
||||||
description: List snapshots linked to a control
|
|
||||||
hints:
|
|
||||||
readonly: true
|
|
||||||
idempotent: true
|
|
||||||
inputSchema:
|
|
||||||
$ref: "#/components/schemas/ListControlSnapshotsInput"
|
|
||||||
outputSchema:
|
|
||||||
$ref: "#/components/schemas/ListControlSnapshotsOutput"
|
|
||||||
- name: listRiskObligations
|
- name: listRiskObligations
|
||||||
description: List obligations linked to a risk
|
description: List obligations linked to a risk
|
||||||
hints:
|
hints:
|
||||||
@@ -10986,32 +10801,6 @@ tools:
|
|||||||
$ref: "#/components/schemas/DeleteTaskInput"
|
$ref: "#/components/schemas/DeleteTaskInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/DeleteTaskOutput"
|
$ref: "#/components/schemas/DeleteTaskOutput"
|
||||||
- name: listSnapshots
|
|
||||||
description: List all snapshots for the organization
|
|
||||||
hints:
|
|
||||||
readonly: true
|
|
||||||
idempotent: true
|
|
||||||
inputSchema:
|
|
||||||
$ref: "#/components/schemas/ListSnapshotsInput"
|
|
||||||
outputSchema:
|
|
||||||
$ref: "#/components/schemas/ListSnapshotsOutput"
|
|
||||||
- name: getSnapshot
|
|
||||||
description: Get a snapshot by ID
|
|
||||||
hints:
|
|
||||||
readonly: true
|
|
||||||
idempotent: true
|
|
||||||
inputSchema:
|
|
||||||
$ref: "#/components/schemas/GetSnapshotInput"
|
|
||||||
outputSchema:
|
|
||||||
$ref: "#/components/schemas/GetSnapshotOutput"
|
|
||||||
- name: takeSnapshot
|
|
||||||
description: Take a snapshot of a collection of objects (risks, vendors, findings, obligations, or processing activities)
|
|
||||||
hints:
|
|
||||||
readonly: false
|
|
||||||
inputSchema:
|
|
||||||
$ref: "#/components/schemas/TakeSnapshotInput"
|
|
||||||
outputSchema:
|
|
||||||
$ref: "#/components/schemas/TakeSnapshotOutput"
|
|
||||||
- name: listDocuments
|
- name: listDocuments
|
||||||
description: List documents for the organization. By default only ACTIVE documents are returned; pass status filter to include ARCHIVED.
|
description: List documents for the organization. By default only ACTIVE documents are returned; pass status filter to include ARCHIVED.
|
||||||
hints:
|
hints:
|
||||||
@@ -11281,6 +11070,14 @@ tools:
|
|||||||
$ref: "#/components/schemas/PublishVendorListInput"
|
$ref: "#/components/schemas/PublishVendorListInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/PublishVendorListOutput"
|
$ref: "#/components/schemas/PublishVendorListOutput"
|
||||||
|
- name: publishRiskList
|
||||||
|
description: Publish the risk register for an organization as a document. If a document already exists, a new version is created.
|
||||||
|
hints:
|
||||||
|
readonly: false
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/PublishRiskListInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/PublishRiskListOutput"
|
||||||
- name: publishStatementOfApplicability
|
- name: publishStatementOfApplicability
|
||||||
description: Publish a statement of applicability as a document. If a document already exists, a new version is created.
|
description: Publish a statement of applicability as a document. If a document already exists, a new version is created.
|
||||||
hints:
|
hints:
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ func NewRisk(r *coredata.Risk) *Risk {
|
|||||||
ResidualLikelihood: r.ResidualLikelihood,
|
ResidualLikelihood: r.ResidualLikelihood,
|
||||||
ResidualImpact: r.ResidualImpact,
|
ResidualImpact: r.ResidualImpact,
|
||||||
ResidualRiskScore: r.ResidualRiskScore,
|
ResidualRiskScore: r.ResidualRiskScore,
|
||||||
SnapshotID: r.SnapshotID,
|
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
UpdatedAt: r.UpdatedAt,
|
UpdatedAt: r.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
|
|
||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewSnapshot(s *coredata.Snapshot) *Snapshot {
|
|
||||||
return &Snapshot{
|
|
||||||
ID: s.ID,
|
|
||||||
OrganizationID: s.OrganizationID,
|
|
||||||
Name: s.Name,
|
|
||||||
Type: s.Type,
|
|
||||||
Description: s.Description,
|
|
||||||
CreatedAt: s.CreatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewListControlSnapshotsOutput(snapshotPage *page.Page[*coredata.Snapshot, coredata.SnapshotOrderField]) ListControlSnapshotsOutput {
|
|
||||||
snapshots := make([]*Snapshot, 0, len(snapshotPage.Data))
|
|
||||||
for _, s := range snapshotPage.Data {
|
|
||||||
snapshots = append(snapshots, NewSnapshot(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextCursor *page.CursorKey
|
|
||||||
if len(snapshotPage.Data) > 0 {
|
|
||||||
cursorKey := snapshotPage.Data[len(snapshotPage.Data)-1].CursorKey(snapshotPage.Cursor.OrderBy.Field)
|
|
||||||
nextCursor = &cursorKey
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListControlSnapshotsOutput{
|
|
||||||
NextCursor: nextCursor,
|
|
||||||
Snapshots: snapshots,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewListSnapshotsOutput(snapshotPage *page.Page[*coredata.Snapshot, coredata.SnapshotOrderField]) ListSnapshotsOutput {
|
|
||||||
snapshots := make([]*Snapshot, 0, len(snapshotPage.Data))
|
|
||||||
for _, s := range snapshotPage.Data {
|
|
||||||
snapshots = append(snapshots, NewSnapshot(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextCursor *page.CursorKey
|
|
||||||
if len(snapshotPage.Data) > 0 {
|
|
||||||
cursorKey := snapshotPage.Data[len(snapshotPage.Data)-1].CursorKey(snapshotPage.Cursor.OrderBy.Field)
|
|
||||||
nextCursor = &cursorKey
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListSnapshotsOutput{
|
|
||||||
NextCursor: nextCursor,
|
|
||||||
Snapshots: snapshots,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,7 +51,6 @@ func NewApplicabilityStatement(a *coredata.ApplicabilityStatement) *Applicabilit
|
|||||||
StatementOfApplicabilityID: a.StatementOfApplicabilityID,
|
StatementOfApplicabilityID: a.StatementOfApplicabilityID,
|
||||||
ControlID: a.ControlID,
|
ControlID: a.ControlID,
|
||||||
OrganizationID: a.OrganizationID,
|
OrganizationID: a.OrganizationID,
|
||||||
SnapshotID: a.SnapshotID,
|
|
||||||
Applicability: a.Applicability,
|
Applicability: a.Applicability,
|
||||||
Justification: a.Justification,
|
Justification: a.Justification,
|
||||||
CreatedAt: a.CreatedAt,
|
CreatedAt: a.CreatedAt,
|
||||||
|
|||||||
Reference in New Issue
Block a user