From cbeaef0079874aa5398d4a4ebb206ffcd1ac4c37 Mon Sep 17 00:00:00 2001 From: Ben Waples Date: Fri, 26 Jan 2024 13:53:39 -0800 Subject: [PATCH 1/2] ESC 9b Finding Panel (#349) * initial setup * some qa * remove hardcoded testing and smol qa * shared help text styles --- .../HelpTexts/ADCSESC10a/General.tsx | 21 +-- .../HelpTexts/ADCSESC10a/LinuxAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC10a/WindowsAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC9a/General.tsx | 21 +-- .../HelpTexts/ADCSESC9a/LinuxAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC9a/WindowsAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC9b/ADCSESC9b.tsx | 31 ++++ .../HelpTexts/ADCSESC9b/General.tsx | 59 +++++++ .../HelpTexts/ADCSESC9b/LinuxAbuse.tsx | 156 +++++++++++++++++ .../components/HelpTexts/ADCSESC9b/Opsec.tsx | 31 ++++ .../HelpTexts/ADCSESC9b/References.tsx | 75 ++++++++ .../HelpTexts/ADCSESC9b/WindowsAbuse.tsx | 164 ++++++++++++++++++ .../src/components/HelpTexts/index.tsx | 2 + .../src/components/HelpTexts/utils.ts | 19 ++ 14 files changed, 549 insertions(+), 110 deletions(-) create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx index b00ca9b1b4..02c61e3567 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx @@ -15,29 +15,12 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import { groupSpecialFormat } from '../utils'; +import { useHelpTextStyles, groupSpecialFormat } from '../utils'; import { EdgeInfoProps } from '../index'; import { Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); const General: FC = ({ sourceName, sourceType, targetName }) => { - const classes = useStyles(); + const classes = useHelpTextStyles(); return ( {groupSpecialFormat(sourceType, sourceName)} has the privileges to perform the ADCS ESC10 Scenario A attack diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx index e7987bc68f..b86b3aa9d4 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx @@ -16,26 +16,10 @@ import { FC } from 'react'; import { Box, Link, List, ListItem, Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const LinuxAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx index fc0652d1ef..dd99094430 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx @@ -15,27 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import makeStyles from '@mui/styles/makeStyles'; import { Typography, Link, List, ListItem, Box } from '@mui/material'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const WindowsAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx index a53caf54b2..6d1b25bb99 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx @@ -15,29 +15,12 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import { groupSpecialFormat } from '../utils'; +import { useHelpTextStyles, groupSpecialFormat } from '../utils'; import { EdgeInfoProps } from '../index'; import { Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); const General: FC = ({ sourceName, sourceType, targetName }) => { - const classes = useStyles(); + const classes = useHelpTextStyles(); return ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx index dbea805e69..f990bed666 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx @@ -16,26 +16,10 @@ import { FC } from 'react'; import { Box, Link, List, ListItem, Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const LinuxAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx index 43bfac3928..844173b1f4 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx @@ -15,27 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import makeStyles from '@mui/styles/makeStyles'; import { Typography, Link, List, ListItem, Box } from '@mui/material'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const WindowsAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx new file mode 100644 index 0000000000..724cfe762e --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx @@ -0,0 +1,31 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import General from './General'; +import WindowsAbuse from './WindowsAbuse'; +import LinuxAbuse from './LinuxAbuse'; +import Opsec from './Opsec'; +import References from './References'; + +const ADCSESC9b = { + general: General, + windowsAbuse: WindowsAbuse, + linuxAbuse: LinuxAbuse, + opsec: Opsec, + references: References, +}; + +export default ADCSESC9b; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx new file mode 100644 index 0000000000..6ca89a6682 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx @@ -0,0 +1,59 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { FC } from 'react'; +import { useHelpTextStyles, groupSpecialFormat } from '../utils'; +import { EdgeInfoProps } from '../index'; +import { Typography } from '@mui/material'; + +const General: FC = ({ sourceName, sourceType, targetName }) => { + const classes = useHelpTextStyles(); + return ( + <> + + {groupSpecialFormat(sourceType, sourceName)} has the privileges to perform the ADCS ESC9 Scenario B + attack against the target domain. +
+
+ The principal has control over a victim computer with permission to enroll on one or more certificate + templates, configured to: 1) enable certificate authentication, 2) require the dNSHostName +  of the enrollee included in the Subject Alternative Name (SAN), and 3) not have the security + extension enabled. The victim computer also has enrollment permission for an enterprise CA with the + necessary templates published. This enterprise CA is trusted for NT authentication in the forest, and + chains up to a root CA for the forest. There is an affected Domain Controller (DC) configured to allow + weak certificate binding enforcement. This setup lets the principal impersonate any AD forest computer + without their credentials. +
+
+ The attacker principal can abuse their control over the victim computer to modify the victim computer's{' '} + dNSHostName attribute to match the dNSHostName of a targeted computer. The + attacker principal will then abuse their control over the victim computer to obtain the credentials of + the victim computer, or a session as the victim computer, and enroll a certificate as the victim in one + of the affected certificate templates. The dNSHostName of the victim will be included in + the issued certificate under SAN DNS name. As the certificate template does not have the security + extension, the issued certificate will NOT include the SID of the victim computer. DCs with strong + certificate binding configuration will require a SID to be present in a certificate used for Kerberos + authentication, but the affected DCs with weak certificate binding configuration will not. The affected + DCs will split the SAN DNS name into a computer name and a domain name, confirm that the domain name is + correct, and use the computer name appended a $ to identify principals with a matching{' '} + sAMAccountName. At last, the DC issues a Kerberos TGT as the targeted computer to the + attacker, which means the attacker now has a session as the targeted computer. +
+ + ); +}; + +export default General; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx new file mode 100644 index 0000000000..fa5a0e84ee --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx @@ -0,0 +1,156 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { FC } from 'react'; +import { Link, Typography } from '@mui/material'; +import { useHelpTextStyles } from '../utils'; + +const LinuxAbuse: FC = () => { + const classes = useHelpTextStyles(); + const step1 = ( + <> + + Step 1: Set dNSHostName of victim computer to targeted computer's{' '} + dNSHostName. +
+
+ Set the dNSHostName of the victim computer using Certipy: +
+ + { + 'certipy account update -username ATTACKER@CORP.LOCAL -password PWD -user VICTIM -dns TARGET.CORP.LOCAL' + } + + + ); + + const step2 = ( + <> + + Step 2: Check if mail attribute of victim must be set and set it if required. +
+
+ If the certificate template is of schema version 2 or above and its attribute{' '} + msPKI-CertificateNameFlag contains the flag SUBJECT_REQUIRE_EMAIL and/or + SUBJECT_ALT_REQUIRE_EMAIL then the victim principal must have their mail attribute set for + the certificate enrollment. The CertTemplate BloodHound node will have "Subject Require Email"{' '} + or "Subject Alternative Name Require Email" set to true if any of the flags are present. +
+
+ If the certificate template is of schema version 1 or does not have any of the email flags, then + continue to Step 3. +
+
+ If any of the two flags are present, you will need the victim's mail attribute to be set. The value of + the attribute will be included in the issues certificate but it is not used to identify the target + computer why it can be set to any arbitrary string. +
+
+ Check if the victim has the mail attribute set using ldapsearch: +
+ {`ldapsearch -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME -b "VICTIM-DN" mail`} + + If the victim has the mail attribute set, continue to Step 3. +
+
+ If the victim does not have the mail attribute set, set it to a dummy mail using ldapmodify: +
+ + {`echo -e "dn: VICTIM-DN\nchangetype: modify\nreplace: mail\nmail: test@mail.com" | ldapmodify -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME`} + + + ); + + const step3 = ( + + Step 3: Obtain a session as victim. +
+
+ There are several options for this step. You can obtain a session as SYSTEM on the host, which allows you to + interact with AD as the computer account, by abusing control over the computer AD object (see{' '} + + GenericAll edge documentation + + ) +
+ ); + + const step4 = ( + <> + + Step 4: Enroll certificate as victim. +
+
+ Use Certipy as the victim computer to request enrollment in the affected template, specifying the + affected EnterpriseCA: +
+ + {'certipy req -u VICTIM@CORP.LOCAL -p PWD -ca CA-NAME -target SERVER -template TEMPLATE'} + + + The issued certificate will be saved to disk with the name of the targeted computer. + + + ); + + const step5 = ( + <> + + Step 5 (Optional): Set dNSHostName of victim to the previous value. +
+
+ To avoid DNS issues in the environment, set the dNSHostName of the victim computer back to + its previous value using Certipy: +
+ + { + 'certipy account update -username ATTACKER@CORP.LOCAL -password PWD -user VICTIM -dns VICTIM.CORP.LOCAL' + } + + + ); + + const step6 = ( + <> + + Step 6: Perform Kerberos authentication as targeted computer against affected DC using + certificate. +
+
+ Request a ticket granting ticket (TGT) from the domain, specifying the certificate created in Step 4 and + the IP of an affected DC: +
+ {'certipy auth -pfx TARGET.pfx -dc-ip IP'} + + ); + + return ( + <> + An attacker may perform this attack in the following steps: + {step1} + {step2} + {step3} + {step4} + {step5} + {step6} + + ); +}; + +export default LinuxAbuse; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx new file mode 100644 index 0000000000..cc73a72e2e --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx @@ -0,0 +1,31 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { FC } from 'react'; +import { Typography } from '@mui/material'; + +const Opsec: FC = () => { + return ( + + When the affected certificate authority issues the certificate to the attacker, it will retain a local copy + of that certificate in its issued certificates store. Defenders may analyze those issued certificates to + identify illegitimately issued certificates and identify the computer that requested the certificate, as + well as the target identity the attacker is attempting to impersonate. + + ); +}; + +export default Opsec; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx new file mode 100644 index 0000000000..1ac6d3fe1c --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx @@ -0,0 +1,75 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import React, { FC } from 'react'; +import { Link, Box } from '@mui/material'; + +const References: FC = () => { + const references = [ + { + label: 'Certipy 4.0', + link: 'https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7', + }, + { + label: 'Certified Pre-Owned', + link: 'https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf', + }, + { + label: 'Certipy', + link: 'https://github.com/ly4k/Certipy', + }, + { + label: 'GhostPack Certipy', + link: 'https://github.com/GhostPack/Certify', + }, + { + label: 'GhostPack Rubeus', + link: 'https://github.com/GhostPack/Rubeus', + }, + { + label: 'Set-DomainObject', + link: 'https://powersploit.readthedocs.io/en/latest/Recon/Set-DomainObject', + }, + { + label: 'CertUtil.exe', + link: 'https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/certutil', + }, + { + label: 'LDAPSearch', + link: 'https://linux.die.net/man/1/ldapsearch', + }, + { + label: 'LDAPModify', + link: 'https://linux.die.net/man/1/ldapmodify', + }, + ]; + return ( + + {references.map((reference) => { + return ( + + + {reference.label} + +
+
+ ); + })} +
+ ); +}; + +export default References; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx new file mode 100644 index 0000000000..35305c6e9e --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx @@ -0,0 +1,164 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { FC } from 'react'; +import { Typography, Link } from '@mui/material'; +import { useHelpTextStyles } from '../utils'; + +const WindowsAbuse: FC = () => { + const classes = useHelpTextStyles(); + const step1 = ( + <> + + Step 1: Set dNSHostName of victim computer to targeted computer's{' '} + dNSHostName. +
+
+ Set the dNSHostName of the victim computer using PowerView: +
+ + {"Set-DomainObject -Identity VICTIM -Set @{'dnshostname'='target.corp.local'}"} + + + ); + + const step2 = ( + <> + + Step 2: Check if mail attribute of victim must be set and set it if required. +
+
+ If the certificate template is of schema version 2 or above and its attribute{' '} + msPKI-CertificateNameFlag contains the flag SUBJECT_REQUIRE_EMAIL and/or{' '} + SUBJECT_ALT_REQUIRE_EMAIL then the victim principal must have their mail{' '} + attribute set for the certificate enrollment. The CertTemplate BloodHound node will have{' '} + "Subject Require Email" or "Subject Alternative Name Require Email" set to true if any + of the flags are present. + "Subject Alternative Name Require Email" set to true if any of the flags are present. +
+
+ If the certificate template is of schema version 1 or does not have any of the email flags, then + continue to Step 3. +
+
+ If any of the two flags are present, you will need the victim's mail attribute to be set. The value of + the attribute will be included in the issues certificate but it is not used to identify the target + computer why it can be set to any arbitrary string. +
+
+ Check if the victim has the mail attribute set using PowerView: +
+ {'Get-DomainObject -Identity VICTIM -Properties mail'} + + If the victim has the mail attribute set, continue to Step 3. +
+
+ If the victim does not have the mail attribute set, set it to a dummy mail using PowerView: +
+ + {"Set-DomainObject -Identity VICTIM -Set @{'mail'='dummy@mail.com'}"} + + + ); + + const step3 = ( + + Step 3: Obtain a session as victim. +
+
+ There are several options for this step. You can obtain a session as SYSTEM on the host, which allows you to + interact with AD as the computer account, by abusing control over the computer AD object (see{' '} + + GenericAll edge documentation + + ). +
+ ); + + const step4 = ( + <> + + Step 4: Enroll certificate as victim. +
+
+ Use Certify as the victim computer to request enrollment in the affected template, specifying the + affected EnterpriseCA: +
+ + {'Certify.exe request /ca:SERVERCA-NAME /template:TEMPLATE /machine'} + + + Save the certificate as cert.pem and the private key as cert.key. + + + ); + + const step5 = ( + <> + + Step 5: Convert the emitted certificate to PFX format: + + {'certutil.exe -MergePFX .cert.pem .cert.pfx'} + + ); + const step6 = ( + <> + + Step 6 (Optional): Set dNSHostName of victim to the previous value. +
+
+ To avoid DNS issues in the environment, set the dNSHostName of the victim computer back to + its previous value using PowerView: +
+ + {"Set-DomainObject -Identity VICTIM -Set @{'dnshostname'='victim.corp.local'}"} + + + ); + const step7 = ( + <> + + Step 7: Perform Kerberos authentication as targeted computer against affected DC using + certificate. +
+
+ Use Rubeus to request a ticket granting ticket (TGT) from an affected DC, specifying the target identity + to impersonate and the PFX-formatted certificate created in Step 5: +
+ + {'Rubeus.exe asktgt /certificate:cert.pfx /user:TARGET$ /domain:DOMAIN /dc:DOMAIN_CONTROLLER'} + + + ); + + return ( + <> + An attacker may perform this attack in the following steps. + {step1} + {step2} + {step3} + {step4} + {step5} + {step6} + {step7} + + ); +}; + +export default WindowsAbuse; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx index d3927d2878..03d4971509 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx @@ -112,6 +112,7 @@ import ADCSESC1 from './ADCSESC1/ADCSESC1'; import ADCSESC6a from './ADCSESC6a/ADCSESC6a'; import ADCSESC6b from './ADCSESC6b/ADCSESC6b'; import ADCSESC9a from './ADCSESC9a/ADCSESC9a'; +import ADCSESC9b from './ADCSESC9b/ADCSESC9b'; import ADCSESC10a from './ADCSESC10a/ADCSESC10a'; export type EdgeInfoProps = { @@ -218,6 +219,7 @@ const EdgeInfoComponents = { ADCSESC6a: ADCSESC6a, ADCSESC6b: ADCSESC6b, ADCSESC9a: ADCSESC9a, + ADCSESC9b: ADCSESC9b, ADCSESC10a: ADCSESC10a, ManageCA: ManageCA, ManageCertificates: ManageCertificates, diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts b/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts index 0ad6a85cb4..7a6caf3abe 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts @@ -14,6 +14,8 @@ // // SPDX-License-Identifier: Apache-2.0 +import { makeStyles } from "@mui/styles"; + export const groupSpecialFormat = (sourceType: string | undefined, sourceName: string | undefined) => { if (!sourceType || !sourceName) return 'This entity has'; if (sourceType === 'Group') { @@ -41,3 +43,20 @@ export const typeFormat = (type: string | undefined): string => { return type.toLowerCase(); } }; + + +export const useHelpTextStyles = makeStyles((theme) => ({ + containsCodeEl: { + '& code': { + backgroundColor: 'darkgrey', + padding: '2px .5ch', + fontWeight: 'normal', + fontSize: '.875em', + borderRadius: '3px', + display: 'inline', + + overflowWrap: 'break-word', + whiteSpace: 'pre-wrap', + }, + }, +})); \ No newline at end of file From a2321982bf151502997c5d43d148854549cfd542 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Mon, 29 Jan 2024 08:55:54 -0800 Subject: [PATCH 2/2] chore: remove unnecessary complexity in the data pipeline (#347) * chore: remove unnecessary complexity in the data pipeline * fix: remove all in-memory state management from data pipeline --- cmd/api/src/api/v2/file_uploads.go | 3 +- cmd/api/src/api/v2/file_uploads_test.go | 9 +- cmd/api/src/daemons/datapipe/datapipe.go | 90 ++++++++------ cmd/api/src/daemons/datapipe/jobs.go | 117 +++++++++--------- cmd/api/src/daemons/datapipe/mocks/mock.go | 12 -- cmd/api/src/model/jobs.go | 5 + .../src/services/fileupload/file_upload.go | 27 ++-- .../src/components/FinishedIngestLog/types.ts | 3 + 8 files changed, 133 insertions(+), 133 deletions(-) diff --git a/cmd/api/src/api/v2/file_uploads.go b/cmd/api/src/api/v2/file_uploads.go index 0f73c1a94a..8a9512fa2e 100644 --- a/cmd/api/src/api/v2/file_uploads.go +++ b/cmd/api/src/api/v2/file_uploads.go @@ -153,10 +153,9 @@ func (s Resources) EndFileUploadJob(response http.ResponseWriter, request *http. api.HandleDatabaseError(request, response, err) } else if fileUploadJob.Status != model.JobStatusRunning { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "job must be in running status to end", request), response) - } else if fileUploadJob, err := fileupload.EndFileUploadJob(s.DB, fileUploadJob); err != nil { + } else if err := fileupload.EndFileUploadJob(s.DB, fileUploadJob); err != nil { api.HandleDatabaseError(request, response, err) } else { - s.TaskNotifier.NotifyOfFileUploadJobStatus(fileUploadJob) response.WriteHeader(http.StatusOK) } } diff --git a/cmd/api/src/api/v2/file_uploads_test.go b/cmd/api/src/api/v2/file_uploads_test.go index 571b6bce0a..f48aca60ed 100644 --- a/cmd/api/src/api/v2/file_uploads_test.go +++ b/cmd/api/src/api/v2/file_uploads_test.go @@ -1,17 +1,17 @@ // Copyright 2023 Specter Ops, Inc. -// +// // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// +// // SPDX-License-Identifier: Apache-2.0 package v2_test @@ -216,7 +216,6 @@ func TestResources_EndFileUploadJob(t *testing.T) { Status: model.JobStatusRunning, }, nil) mockDB.EXPECT().UpdateFileUploadJob(gomock.Any()).Return(nil) - mockTasker.EXPECT().NotifyOfFileUploadJobStatus(gomock.Any()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusOK) diff --git a/cmd/api/src/daemons/datapipe/datapipe.go b/cmd/api/src/daemons/datapipe/datapipe.go index 2baf804def..a47ee54a39 100644 --- a/cmd/api/src/daemons/datapipe/datapipe.go +++ b/cmd/api/src/daemons/datapipe/datapipe.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "sync" + "sync/atomic" "time" "github.com/specterops/bloodhound/cache" @@ -40,24 +41,19 @@ const ( ) type Tasker interface { - NotifyOfFileUploadJobStatus(task model.FileUploadJob) RequestAnalysis() GetStatus() model.DatapipeStatusWrapper } type Daemon struct { - db database.Database - graphdb graph.Database - cache cache.Cache - cfg config.Configuration - analysisRequested bool - tickInterval time.Duration - status model.DatapipeStatusWrapper - ctx context.Context - fileUploadJobIDsUnderAnalysis []int64 - completedFileUploadJobIDs []int64 - - lock *sync.Mutex + db database.Database + graphdb graph.Database + cache cache.Cache + cfg config.Configuration + analysisRequested *atomic.Bool + tickInterval time.Duration + status model.DatapipeStatusWrapper + ctx context.Context clearOrphanedFilesLock *sync.Mutex } @@ -67,14 +63,12 @@ func (s *Daemon) Name() string { func NewDaemon(ctx context.Context, cfg config.Configuration, connections bootstrap.DatabaseConnections[*database.BloodhoundDB, *graph.DatabaseSwitch], cache cache.Cache, tickInterval time.Duration) *Daemon { return &Daemon{ - db: connections.RDMS, - graphdb: connections.Graph, - cache: cache, - cfg: cfg, - ctx: ctx, - - analysisRequested: false, - lock: &sync.Mutex{}, + db: connections.RDMS, + graphdb: connections.Graph, + cache: cache, + cfg: cfg, + ctx: ctx, + analysisRequested: &atomic.Bool{}, clearOrphanedFilesLock: &sync.Mutex{}, tickInterval: tickInterval, status: model.DatapipeStatusWrapper{ @@ -93,42 +87,41 @@ func (s *Daemon) GetStatus() model.DatapipeStatusWrapper { } func (s *Daemon) getAnalysisRequested() bool { - s.lock.Lock() - defer s.lock.Unlock() - return s.analysisRequested + return s.analysisRequested.Load() } func (s *Daemon) setAnalysisRequested(requested bool) { - s.lock.Lock() - defer s.lock.Unlock() - s.analysisRequested = requested + s.analysisRequested.Store(requested) } func (s *Daemon) analyze() { + // Ensure that the user-requested analysis switch is flipped back to false. This is done at the beginning of the + // function so that any re-analysis requests are caught while analysis is in-progress. + s.setAnalysisRequested(false) + if s.cfg.DisableAnalysis { return } s.status.Update(model.DatapipeStatusAnalyzing, false) - log.Measure(log.LevelInfo, "Starting analysis")() + log.LogAndMeasure(log.LevelInfo, "Graph Analysis")() if err := RunAnalysisOperations(s.ctx, s.db, s.graphdb, s.cfg); err != nil { - log.Errorf("Analysis failed: %v", err) + log.Errorf("Graph analysis failed: %v", err) s.failJobsUnderAnalysis() s.status.Update(model.DatapipeStatusIdle, false) } else { + s.completeJobsUnderAnalysis() + if entityPanelCachingFlag, err := s.db.GetFlagByKey(appcfg.FeatureEntityPanelCaching); err != nil { log.Errorf("Error retrieving entity panel caching flag: %v", err) } else { resetCache(s.cache, entityPanelCachingFlag.Enabled) } - s.clearJobsFromAnalysis() - log.Measure(log.LevelInfo, "Analysis run finished")() + s.status.Update(model.DatapipeStatusIdle, true) } - - s.setAnalysisRequested(false) } func resetCache(cacher cache.Cache, cacheEnabled bool) { @@ -143,10 +136,22 @@ func (s *Daemon) ingestAvailableTasks() { if ingestTasks, err := s.db.GetAllIngestTasks(); err != nil { log.Errorf("Failed fetching available ingest tasks: %v", err) } else { - s.processIngestTasks(ingestTasks) + s.processIngestTasks(s.ctx, ingestTasks) } } +func (s *Daemon) getNumJobsWaitingForAnalysis() (int, error) { + numJobsWaitingForAnalysis := 0 + + if fileUploadJobsUnderAnalysis, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusAnalyzing); err != nil { + return 0, err + } else { + numJobsWaitingForAnalysis += len(fileUploadJobsUnderAnalysis) + } + + return numJobsWaitingForAnalysis, nil +} + func (s *Daemon) Start() { var ( datapipeLoopTimer = time.NewTimer(s.tickInterval) @@ -164,15 +169,20 @@ func (s *Daemon) Start() { s.clearOrphanedData() case <-datapipeLoopTimer.C: + // Ingest all available ingest tasks + s.ingestAvailableTasks() + + // Manage time-out state progression for file upload jobs fileupload.ProcessStaleFileUploadJobs(s.db) - if s.numAvailableCompletedFileUploadJobs() > 0 { - s.processCompletedFileUploadJobs() - s.analyze() - } else if s.getAnalysisRequested() { + // Manage nominal state transitions for file upload jobs + s.processIngestedFileUploadJobs() + + // If there are completed file upload jobs or if analysis was user-requested, perform analysis. + if numJobsWaitingForAnalysis, err := s.getNumJobsWaitingForAnalysis(); err != nil { + log.Errorf("Failed looking up jobs waiting for analysis: %v", err) + } else if numJobsWaitingForAnalysis > 0 || s.getAnalysisRequested() { s.analyze() - } else { - s.ingestAvailableTasks() } datapipeLoopTimer.Reset(s.tickInterval) diff --git a/cmd/api/src/daemons/datapipe/jobs.go b/cmd/api/src/daemons/datapipe/jobs.go index 908edd3a41..b80a028c98 100644 --- a/cmd/api/src/daemons/datapipe/jobs.go +++ b/cmd/api/src/daemons/datapipe/jobs.go @@ -17,6 +17,7 @@ package datapipe import ( + "context" "os" "github.com/specterops/bloodhound/dawgs/graph" @@ -25,78 +26,88 @@ import ( "github.com/specterops/bloodhound/src/services/fileupload" ) -func (s *Daemon) numAvailableCompletedFileUploadJobs() int { - s.lock.Lock() - defer s.lock.Unlock() - - return len(s.completedFileUploadJobIDs) -} - func (s *Daemon) failJobsUnderAnalysis() { - for _, jobID := range s.fileUploadJobIDsUnderAnalysis { - if err := fileupload.FailFileUploadJob(s.db, jobID, "Analysis failed"); err != nil { - log.Errorf("Failed updating job %d to failed status: %v", jobID, err) + if fileUploadJobsUnderAnalysis, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusAnalyzing); err != nil { + log.Errorf("Failed to load file upload jobs under analysis: %v", err) + } else { + for _, job := range fileUploadJobsUnderAnalysis { + if err := fileupload.FailFileUploadJob(s.db, job.ID, "Analysis failed"); err != nil { + log.Errorf("Failed updating file upload job %d to failed status: %v", job.ID, err) + } } } - - s.clearJobsFromAnalysis() -} - -func (s *Daemon) clearJobsFromAnalysis() { - s.lock.Lock() - s.fileUploadJobIDsUnderAnalysis = s.fileUploadJobIDsUnderAnalysis[:0] - s.lock.Unlock() } -func (s *Daemon) processCompletedFileUploadJobs() { - completedJobIDs := s.getAndTransitionCompletedJobIDs() - - for _, id := range completedJobIDs { - if ingestTasks, err := s.db.GetIngestTasksForJob(id); err != nil { - log.Errorf("Failed fetching available ingest tasks: %v", err) - } else { - s.processIngestTasks(ingestTasks) +func (s *Daemon) completeJobsUnderAnalysis() { + if fileUploadJobsUnderAnalysis, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusAnalyzing); err != nil { + log.Errorf("Failed to load file upload jobs under analysis: %v", err) + } else { + for _, job := range fileUploadJobsUnderAnalysis { + if err := fileupload.UpdateFileUploadJobStatus(s.db, job, model.JobStatusComplete, "Complete"); err != nil { + log.Errorf("Error updating fileupload job %d: %v", job.ID, err) + } } + } +} - if err := fileupload.UpdateFileUploadJobStatus(s.db, id, model.JobStatusComplete, "Complete"); err != nil { - log.Errorf("Error updating fileupload job %d: %v", id, err) +func (s *Daemon) processIngestedFileUploadJobs() { + if ingestedFileUploadJobs, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusIngesting); err != nil { + log.Errorf("Failed to look up finished file upload jobs: %v", err) + } else { + for _, ingestedFileUploadJob := range ingestedFileUploadJobs { + if err := fileupload.UpdateFileUploadJobStatus(s.db, ingestedFileUploadJob, model.JobStatusAnalyzing, "Analyzing"); err != nil { + log.Errorf("Error updating fileupload job %d: %v", ingestedFileUploadJob.ID, err) + } } } } -func (s *Daemon) getAndTransitionCompletedJobIDs() []int64 { - s.lock.Lock() - defer s.lock.Unlock() +// clearFileTask removes a generic file upload task for ingested data. +func (s *Daemon) clearFileTask(ingestTask model.IngestTask) { + if err := s.db.DeleteIngestTask(ingestTask); err != nil { + log.Errorf("Error removing file upload task from db: %v", err) + } +} - // transition completed jobs to analysis - s.fileUploadJobIDsUnderAnalysis = append(s.fileUploadJobIDsUnderAnalysis, s.completedFileUploadJobIDs...) - s.completedFileUploadJobIDs = s.completedFileUploadJobIDs[:0] +func (s *Daemon) processIngestFile(ctx context.Context, path string) error { + if jsonFile, err := os.Open(path); err != nil { + return err + } else { + defer func() { + if err := jsonFile.Close(); err != nil { + log.Errorf("Failed closing ingest file %s: %v", path, err) + } + }() - return s.fileUploadJobIDsUnderAnalysis + return s.graphdb.BatchOperation(ctx, func(batch graph.Batch) error { + if err := s.ReadWrapper(batch, jsonFile); err != nil { + return err + } else { + return nil + } + }) + } } -func (s *Daemon) processIngestTasks(ingestTasks model.IngestTasks) { +// processIngestTasks covers the generic file upload case for ingested data. +func (s *Daemon) processIngestTasks(ctx context.Context, ingestTasks model.IngestTasks) { s.status.Update(model.DatapipeStatusIngesting, false) defer s.status.Update(model.DatapipeStatusIdle, false) for _, ingestTask := range ingestTasks { - jsonFile, err := os.Open(ingestTask.FileName) - if err != nil { - log.Errorf("Error reading file for ingest task %v: %v", ingestTask.ID, err) + // Check the context to see if we should continue processing ingest tasks. This has to be explicit since error + // handling assumes that all failures should be logged and not returned. + select { + case <-ctx.Done(): + return + default: } - if err = s.graphdb.BatchOperation(s.ctx, func(batch graph.Batch) error { - if err := s.ReadWrapper(batch, jsonFile); err != nil { - return err - } else { - return nil - } - }); err != nil { - log.Errorf("Error processing ingest task %v: %v", ingestTask.ID, err) + if err := s.processIngestFile(ctx, ingestTask.FileName); err != nil { + log.Errorf("Failed processing ingest task %d with file %s: %v", ingestTask.ID, ingestTask.FileName, err) } - s.clearTask(ingestTask) - jsonFile.Close() + s.clearFileTask(ingestTask) } } @@ -105,11 +116,3 @@ func (s *Daemon) clearTask(ingestTask model.IngestTask) { log.Errorf("Error removing task from db: %v", err) } } - -func (s *Daemon) NotifyOfFileUploadJobStatus(job model.FileUploadJob) { - if job.Status == model.JobStatusIngesting { - s.lock.Lock() - s.completedFileUploadJobIDs = append(s.completedFileUploadJobIDs, job.ID) - s.lock.Unlock() - } -} diff --git a/cmd/api/src/daemons/datapipe/mocks/mock.go b/cmd/api/src/daemons/datapipe/mocks/mock.go index 8148722ec5..2c9a4d61a9 100644 --- a/cmd/api/src/daemons/datapipe/mocks/mock.go +++ b/cmd/api/src/daemons/datapipe/mocks/mock.go @@ -64,18 +64,6 @@ func (mr *MockTaskerMockRecorder) GetStatus() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatus", reflect.TypeOf((*MockTasker)(nil).GetStatus)) } -// NotifyOfFileUploadJobStatus mocks base method. -func (m *MockTasker) NotifyOfFileUploadJobStatus(arg0 model.FileUploadJob) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "NotifyOfFileUploadJobStatus", arg0) -} - -// NotifyOfFileUploadJobStatus indicates an expected call of NotifyOfFileUploadJobStatus. -func (mr *MockTaskerMockRecorder) NotifyOfFileUploadJobStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotifyOfFileUploadJobStatus", reflect.TypeOf((*MockTasker)(nil).NotifyOfFileUploadJobStatus), arg0) -} - // RequestAnalysis mocks base method. func (m *MockTasker) RequestAnalysis() { m.ctrl.T.Helper() diff --git a/cmd/api/src/model/jobs.go b/cmd/api/src/model/jobs.go index bb305f37ff..4a0ca0841e 100644 --- a/cmd/api/src/model/jobs.go +++ b/cmd/api/src/model/jobs.go @@ -116,6 +116,7 @@ const ( JobStatusTimedOut JobStatus = 4 JobStatusFailed JobStatus = 5 JobStatusIngesting JobStatus = 6 + JobStatusAnalyzing JobStatus = 7 ) func allJobStatuses() []JobStatus { @@ -128,6 +129,7 @@ func allJobStatuses() []JobStatus { JobStatusTimedOut, JobStatusFailed, JobStatusIngesting, + JobStatusAnalyzing, } } @@ -166,6 +168,9 @@ func (s JobStatus) String() string { case JobStatusIngesting: return "INGESTING" + case JobStatusAnalyzing: + return "ANALYZING" + default: return "INVALIDSTATUS" } diff --git a/cmd/api/src/services/fileupload/file_upload.go b/cmd/api/src/services/fileupload/file_upload.go index f1b7769cb5..6517547321 100644 --- a/cmd/api/src/services/fileupload/file_upload.go +++ b/cmd/api/src/services/fileupload/file_upload.go @@ -110,29 +110,22 @@ func TouchFileUploadJobLastIngest(db FileUploadData, fileUploadJob model.FileUpl return db.UpdateFileUploadJob(fileUploadJob) } -func EndFileUploadJob(db FileUploadData, job model.FileUploadJob) (model.FileUploadJob, error) { +func EndFileUploadJob(db FileUploadData, job model.FileUploadJob) error { job.Status = model.JobStatusIngesting + if err := db.UpdateFileUploadJob(job); err != nil { - return job, fmt.Errorf("error ending file upload job: %w", err) - } else { - return job, nil + return fmt.Errorf("error ending file upload job: %w", err) } -} -func UpdateFileUploadJobStatus(db FileUploadData, jobID int64, status model.JobStatus, message string) error { - if job, err := db.GetFileUploadJob(jobID); err != nil { - return err - } else { - job.Status = status - job.StatusMessage = message - job.EndTime = time.Now().UTC() - - return db.UpdateFileUploadJob(job) - } + return nil } -func CompleteFileUploadJob(db FileUploadData, jobID int64) (model.FileUploadJob, error) { - return model.FileUploadJob{}, nil +func UpdateFileUploadJobStatus(db FileUploadData, fileUploadJob model.FileUploadJob, status model.JobStatus, message string) error { + fileUploadJob.Status = status + fileUploadJob.StatusMessage = message + fileUploadJob.EndTime = time.Now().UTC() + + return db.UpdateFileUploadJob(fileUploadJob) } func TimeOutUploadJob(db FileUploadData, jobID int64, message string) error { diff --git a/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts b/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts index 677a7a9e2c..0e7f785072 100644 --- a/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts +++ b/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts @@ -34,7 +34,9 @@ export enum FileUploadJobStatus { TIMED_OUT = 4, FAILED = 5, INGESTING = 6, + ANALYZING = 7, } + export const FileUploadJobStatusToString: Record = { [-1]: 'Invalid', 0: 'Ready', @@ -44,4 +46,5 @@ export const FileUploadJobStatusToString: Record = 4: 'Timed Out', 5: 'Failed', 6: 'Ingesting', + 7: 'Analyzing', };