These scripts grep the Artifactory access logs for user access, the generated output is then formatted via logstash and finally converted to a Excel spreadsheet, during which Active Directory is check to see if the users account is disabled or enabled.

The reason for this script was created is that security needed to ensure that no disabled users had been trying to access Artifactory from outside of the network after they had left the company.

Steps

  1. CheckUsersRequests.sh
  2. FormatLogs.sh (logstash is needed)
  3. CreateExcelDoc.ps1 (excel is needed)
#!/bin/bash

# This script greps the Artifactory logs for users list in argument file (e.g. "search_users.txt") and for each generates a "username" file.
#
# e.g.
#  ./CheckUsersRequests.sh search_users.txt
#
# Cygwin
#  ./CheckUsersRequests.sh search_users.txt /cygdrive/c/Users/john.newman/Desktop/Artifactory_Logs/
#  ./CheckUsersRequests.sh search_users.txt /cygdrive/c/Users/john.newman/Desktop/Artifactory_Logs/ /cygdrive/c/temp/_per-user_logs
#
# As command "zgrep" is used a Linux box or Cygwin is required.
#
# NOTE: The output log files are used by script "CreateExcelDoc.ps1" with JSON files created for debugging.
#

usernames_file="$1"
: ${usernames_file:?"You MUST provide an file with usernames to search"}

log_file_path="$2"
: "/opt/artifactory/logs"

output_path="$3"
: "_per-user_logs"

if [ ! -d "$output_path" ]; then
  mkdir "$output_path"
fi

while read username
do
  echo "$username"
  grep "$username" $log_file_path/access.log > "$output_path/$username.txt"
  zgrep "$username" $log_file_path/access.*.log.zip >> "$output_path/$username.txt"
done < "$usernames_file"

input {
  stdin {
    type => "apache"
  }
}

filter {
  if [type] == "apache" {
    kv {
      trim => " "
    }

    grok {
      match => [
                "message", "%{DATESTAMP:datestamp}%{SPACE}\[%{GREEDYDATA:request_type}\]%{SPACE}%{NOTSPACE:repo}\:(?<package>[a-zA-Z\.]*([^\.][\d])*)\.(?<version>\d([0-9\.])+)\-%{NOTSPACE:prerelease}\.nupkg%{SPACE}for%{SPACE}%{NOTSPACE:user}\/%{IPORHOST:remote}\.",
                "message", "%{DATESTAMP:datestamp}%{SPACE}\[%{GREEDYDATA:request_type}\]%{SPACE}%{NOTSPACE:repo}\:(?<package>[a-zA-Z\.]*([^\.][\d])*)\.(?<version>\d([0-9\.])+)\.nupkg%{SPACE}for%{SPACE}%{NOTSPACE:user}\/%{IPORHOST:remote}\.",
                "message", "%{DATESTAMP:datestamp}%{SPACE}\[%{GREEDYDATA:request_type}\]%{SPACE}%{GREEDYDATA:msg}%{SPACE}for%{SPACE}%{NOTSPACE:user}\/%{IPORHOST:remote}\.",
                "message", "%{DATESTAMP:datestamp}%{SPACE}\[%{GREEDYDATA:request_type}\]%{SPACE}for%{SPACE}%{NOTSPACE:user}\/%{IPORHOST:remote}\.",
                "message", "%{DATESTAMP:datestamp}%{SPACE}\[%{GREEDYDATA:request_type}\]%{SPACE}for%{SPACE}%{NOTSPACE:user}\."
               ]
    }

    date {
      match => [ "datestamp", "YYYY-MM-dd HH:mm:ss,SSS" ]
    }
  }

  if ![repo] {
    mutate {
      add_field => { "repo" => "" }
    }
  }

  if ![package] {
    mutate {
      add_field => { "package" => "" }
    }
  }

  if ![version] {
    mutate {
      add_field => { "version" => "" }
    }
  }

  if ![prerelease] {
    mutate {
      add_field => { "prerelease" => "" }
    }
  }

  if ![msg] {
    mutate {
      add_field => { "msg" => "" }
    }
  }

  if ![tags] {
    mutate {
      add_field => { "tags" => "" }
    }
  }
}

output {
  if "_grokparsefailure" in [tags] {
    stdout { }
  } else {
    file {
      path => "output.json"
    }
    file {
      path => "output.txt"
      flush_interval => 0

      message_format => "%{+dd/MM/YY}   %{+HH:mm:ss}    %{request_type} %{user} %{remote}   %{repo} %{package}  %{version}  %{prerelease}   %{msg}  %{tags}"
    }
  }
}

#!/bin/bash

# This script uses logstash to format / normalized files that are generated by script "CheckUsersRequests.sh" and selects the files based on users listed in argument file (e.g. "search_users.txt").
#
# e.g.
#  ./FormatLogs.sh search_users.txt
#
# Bash (Windows)
#  ./FormatLogs.sh search_users.txt /c/temp/_per-user_logs /s/Dev/logstash/logstash-1.3.2-flatjar.jar /c/temp/_output_formatted /c/temp/_temp
#
# NOTE: Cygwin does not work with Java
#

usernames_file="$1"
: ${usernames_file:?"You MUST provide an file with usernames to search"}

input_path="$2"
: "_Output"

logstash_jar_file="$3"
: "/s/Dev/logstash/logstash-1.3.2-flatjar.jar"

output_path="$4"
: "_Output_Formatted"

temp_path="$5"
: "_Temp"

java_path="java"
logstash_config_file="logstash/logstash.conf"

if [ ! -d "$output_path" ]; then
  mkdir "$output_path"
fi

if [ ! -d "$temp_path" ]; then
  mkdir "$temp_path"
fi

echo "date   time    request_type    user    remote  repo    package version prerelease  msg tags" > "header.txt"

while read username
do
  if [ ! -e "$input_path/$username.txt" ]; then
    echo "ERROR: File not found: $input_path/$username.txt"
  else
    echo "$username"

    # Remove log path from file line
    sed "s/^.*\/access.*.log.zip://" "$input_path/$username.txt" > "$temp_path/$username.txt"

    ${java_path} -Xmx512m -jar ${logstash_jar_file} agent -f "$logstash_config_file" < "$temp_path/$username.txt"

    if [ -e "output.json" ]; then
      mv "output.json" "$output_path/$username.json"
    fi

    if [ -e "output.txt" ]; then
      cat "header.txt" "output.txt" > "output.log"
      rm -f "output.txt"
      mv "output.log" "$output_path/$username.log"
    fi
  fi
done < "$usernames_file"

if [ -e "header.txt" ]; then
  rm -f "header.txt"
fi

[CmdletBinding()]
Param(
    [parameter(Mandatory=$True,Position=0)]
    [alias("I")]
    $InputFiles,
    [parameter(Mandatory=$False,Position=1)]
    [alias("O")]
    [String]$OutputFile = ""
)

# This scrip creates an Excel Spreadsheet from formatted "username" files that are generated by script "FormatLogs.sh"
# It checks Active Directiory for account actions, like "LastLogonDate".
#
# e.g.
#  .\CreateExcelDoc.ps1 -InputFiles (Get-ChildItem -Path "C:\temp\_output_formatted" -Filter "*.log" | ?{ !$_.PSIsContainer }) -OutputFile "Artifactory_Access_Users.xlsx"
#

$script:Path = $(Split-Path -parent $MyInvocation.MyCommand.Definition)
$script:ModulePath = $(Join-Path -Path "$script:Path" -ChildPath ".\modules")

if (!([string]::IsNullOrEmpty($OutputFile))) {
  if (!([System.IO.Path]::IsPathRooted("$OutputFile"))) {
    $OutputFile = $(Join-Path -Path $(Get-Location) -ChildPath $([System.IO.Path]::GetFileName("$OutputFile")))
  }
}

function Release-Ref ($ref) {
  ([System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$ref) -gt 0)
}

function Insert-ADDetails(
  [parameter(Mandatory=$True)]
  [System.__ComObject]$worksheet,
  [parameter(Mandatory=$True)]
  [String]$identity)
{
  $xlShiftDown = [Microsoft.Office.Interop.Excel.XlInsertShiftDirection]::xlShiftDown

  $props = @("AccountExpirationDate", "Created", "LastBadPasswordAttempt", "LastLogonDate", "Modified", "PasswordLastSet", "whenChanged", "Enabled")
  $userADProps = Get-ADUser -identity "$identity" -Properties $props

  if ($userADProps -ne $null) {
    foreach ($prop in $props) {
      $propValue = $userADProps.("$prop")
      $propDate = "{0:dd/MM/yyyy}" -f ($propValue)
      $propTime = "{0:HH:mm:ss}" -f ($propValue)
      $propAdAction = "AD ACTION"

      if (!([string]::IsNullOrEmpty($propValue))) {
        $eRow = $worksheet.cells.item(2,1).entireRow
        $active = $eRow.activate()
        $active = $eRow.insert($xlShiftDown)

        $rowColor = 37
        if ("$prop" -eq "Enabled") {
          $propDate = " " # NOTE: Space is used so this row is placed at the top after sorting.
          $propTime = " " # NOTE: Space is used so this row is placed at the top after sorting.
          if ($propValue -eq $False) {
            $rowColor = 3
            $prop = "Disabled"
            $worksheet.Tab.ColorIndex = 3
          } else {
            $rowColor = 4
          }
        }

        $columnMax = ($worksheet.usedRange.columns).count
        for($column = 1 ; $column -le $columnMax ; $column ++) {
          $worksheet.cells.item(2,$column).Interior.ColorIndex = $rowColor
        }

        $worksheet.Cells.Item(2,1) = ($propDate)
        $worksheet.Cells.Item(2,2) = ($propTime)
        $worksheet.Cells.Item(2,3) = $propAdAction
        $worksheet.Cells.Item(2,4) = "$prop"
      }
    }
  }
}

function Sort-Feild(
  [parameter(Mandatory=$True)]
  [System.__ComObject]$worksheet)
{
  $xlYes = 1
  $xlNo = 2

  $xlSortOnValues = $xlSortNormal = 0
  $xlTopToBottom = $xlSummaryBelow = 1

  $xlAscending = 1
  $xlDescending = 2

  $worksheet.sort.sortFields.clear()

  $usedRange = $worksheet.UsedRange

  [void]$worksheet.sort.sortFields.add($worksheet.Range("A1"), $xlSortOnValues, $xlDescending, $xlSortNormal)
  $worksheet.sort.setRange($worksheet.UsedRange)
  $worksheet.sort.header = $xlYes
  $worksheet.sort.orientation = $xlTopToBottom
  $worksheet.sort.apply()

  [void]$worksheet.Range("A1").Select()
}

function Add-worksheet(
  [parameter(Mandatory=$True)]
  [System.__ComObject]$worksheets,
  [parameter(Mandatory=$True)]
  [String]$file)
{
  $name = [System.IO.Path]::GetFileNameWithoutExtension("$file")

  $worksheet = $worksheets.add([System.Reflection.Missing]::Value,$worksheets.Item($worksheets.count))
  $worksheet.Name = "$name"

  $TxtConnector = ("TEXT;$file")
  $CellRef = $worksheet.Range("A1")

  $Connector = $worksheet.QueryTables.add($TxtConnector,$CellRef)
  $worksheet.QueryTables.item("$($Connector.name)").TextFileTabDelimiter = $True
  $worksheet.QueryTables.item("$($Connector.name)").TextFileParseType = 1
  [void]$worksheet.QueryTables.item("$($Connector.name)").Refresh()
  $worksheet.QueryTables.item("$($Connector.name)").delete()

  Insert-ADDetails -worksheet $worksheet `
                   -identity "$name"

  $excel.Rows.Item(1).Font.Bold = $true
  [void]$excel.Cells.EntireColumn.AutoFilter()
  [void]$worksheet.UsedRange.EntireColumn.AutoFit()

  $worksheet.Activate();
  $worksheet.Application.ActiveWindow.SplitRow = 1;
  $worksheet.Application.ActiveWindow.FreezePanes = $True;

  Sort-Feild -worksheet $worksheet

  $a = Release-Ref($worksheet)
}

function Main
{
  $excel = New-Object -ComObject excel.application
  $excel.visible = ([string]::IsNullOrEmpty($OutputFile))
  $excel.DisplayAlerts = $False

  $workbooks = $excel.Workbooks.Add()
  $worksheets = $workbooks.worksheets

  while ($workbooks.sheets.count -gt 1) {
    $worksheets.Item($workbooks.sheets.count).delete()
  }

  foreach ($file in $InputFiles) {
    Write-Host "$($file.FullName)"
    Add-worksheet -worksheets $worksheets `
                  -file "$($file.FullName)"
  }

  $worksheets.Item(1).delete()
  $worksheets.Item(1).Select()

  if (!([string]::IsNullOrEmpty($OutputFile))) {
    $xlFixedFormat = [Microsoft.Office.Interop.Excel.XlFileFormat]::xlWorkbookDefault

    $workbooks.SaveAs("$OutputFile", $xlFixedFormat)
    $excel.Quit()
  }

  $a = Release-Ref($workbooks)
  $a = Release-Ref($excel)
  $excel = $null

  [System.GC]::Collect()
  [System.GC]::WaitForPendingFinalizers()
}

Main