Public/Attire-ExecutionLogger.psm1

# Attire-ExecutionLogger.psm1
# Copyright 2023 Security Risk Advisors

# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”),
# to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

$script:attireLog = [PSCustomObject]@{
    'attire-version'     = '1.1'
    'execution-data' = ''
    'procedures'    = @()
}

function Start-ExecutionLog($startTime, $logPath, $targetHostname, $targetUser, $commandLine, $isWindows) {

    $ipAddress = Get-PreferredIPAddress $isWindows

    if($targetUser -isnot [string]) {
        if([bool]($targetUser.PSobject.Properties.name -match "^value$")) {
            $targetUser = $targetUser.value
        }else {
            $targetUser = $targetUser.ToString()
        }
    }
    if($targetHostname -isnot [string]) {
        if([bool]($targetHostname.PSobject.Properties.name -match "^value$")) {
            $targetHostname = $targetHostname.value
        }else {
            $targetHostname = $targetHostname.ToString()
        }
    }

    $target = [PSCustomObject]@{
        user = $targetUser
        host = $targetHostname
        ip = $ipAddress
        path = $Env:PATH
    }

    $guid = New-Guid
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($guid.Guid)
    $executionId = [Convert]::ToBase64String($bytes)

    $executionCategory = [PSCustomObject]@{
        'name' = "Atomic Red Team"
        'abbreviation' = "ART"
    }

    $executionData = [PSCustomObject]@{
        'execution-source' = "Invoke-Atomicredteam"
        'execution-id' = $executionId
        'execution-category' = $executionCategory
        'execution-command' = $commandLine
        target = $target
        'time-generated' = ""
    }

    $script:attireLog.'execution-data' = $executionData
}

function Write-ExecutionLog($startTime, $stopTime, $technique, $testNum, $testName, $testGuid, $testExecutor, $testDescription, $command, $logPath, $targetHostname, $targetUser, $res, $isWindows) {

    $startTime = (Get-Date($startTime).ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z').ToString()
    $stopTime = (Get-Date($stopTime).ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z').ToString()

    $procedureId = [PSCustomObject]@{
        type = "guid"
        id = $testGuid
    }

    $step = [PSCustomObject]@{
        'order' = 1
        'time-start' = $startTime
        'time-stop' = $stopTime
        'executor' = $testExecutor
        'command' = $command
        'output' = @()
    }

    $stdOutContents = $res.StandardOutput
    if(($stdOutContents -isnot [string]) -and ($stdOutContents -ne $null)) {
        $stdOutContents = $stdOutContents.ToString()
    }

    $outputStdConsole = [PSCustomObject]@{
        content = $stdOutContents
        level = "STDOUT"
        type = "console"
    }

    $stdErrContents = $res.ErrorOutput
    if(($stdErrContents -isnot [string]) -and ($stdErrContents -ne $null)) {
        $stdErrContents = $stdErrContents.ToString()
    }

    $outputErrConsole = [PSCustomObject]@{
        content = $stdErrContents
        level = "STDERR"
        type = "console"
    }

    [bool] $foundOutput = $false
    if($res.StandardOutput.length -gt 0) {
        $foundOutput = $true
        $step.output += $outputStdConsole
    }

    if($res.ErrorOutput.length -gt 0) {
        $foundOutput = $true
        $step.output += $outputErrConsole
    }

    if (!$foundOutput) {
        $emptyOutput = [PSCustomObject]@{
            content = ""
            level = "STDOUT"
            type = "console"
        }
        $step.output += $emptyOutput
    }

    $procedure = [PSCustomObject]@{
        'mitre-technique-id' = $technique
        'procedure-name' = $testName
        'procedure-id' = $procedureId
        'procedure-description' = $testDescription
        order = $testNum
        steps = @()
    }

    $procedure.steps += $step

    $script:attireLog.procedures += $procedure
}

function Stop-ExecutionLog($startTime, $logPath, $targetHostname, $targetUser, $isWindows) {
    $script:attireLog.'execution-data'.'time-generated' = (Get-Date (Get-Date).ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z')
    #$script:attireLog | Export-Csv -Path "attireLogObject.csv"
    $content = ($script:attireLog | ConvertTo-Json -Depth 12)
    #$Utf8NoBom = New-Object System.Text.UTF8Encoding $False
    [System.IO.File]::WriteAllLines((Resolve-NonexistantPath($logPath)), $content)
    #Out-File -FilePath $logPath -InputObject ($script:attireLog | ConvertTo-Json -Depth 12) -Append -Encoding ASCII
    $script:attireLog = [PSCustomObject]@{
        'attire-version'     = '1.1'
        'execution-data' = ''
        procedures    = @()
    }
}

function Resolve-NonexistantPath($File) {
    $Path = Resolve-Path $File -ErrorAction SilentlyContinue -ErrorVariable error

    if(-not($Path)) {
        $Path = $error[0].TargetObject
    }

    return $Path
}