├── CatalogTools ├── CatalogTools.psd1 ├── CatalogTools.psm1 ├── Lib │ └── BouncyCastle.Crypto.dll └── Parsers │ └── CATParser.ps1 ├── LICENSE ├── LICENSE_Bouncy_Castles └── README.md /CatalogTools/CatalogTools.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'CatalogTools.psm1' 3 | 4 | ModuleVersion = '0.1.0.0' 5 | 6 | GUID = '8c68d3af-334f-48b7-a98f-e276b4309c03' 7 | 8 | Author = 'Matthew Graeber' 9 | 10 | Copyright = 'BSD 3-Clause' 11 | 12 | Description = 'CatalogTools is a module to assist in parsing and managing catalog files.' 13 | 14 | PowerShellVersion = '3.0' 15 | 16 | RequiredAssemblies = @('Lib\BouncyCastle.Crypto.dll') 17 | 18 | # Functions to export from this module 19 | FunctionsToExport = @( 20 | 'Get-CatalogFile' 21 | ) 22 | 23 | PrivateData = @{ 24 | 25 | PSData = @{ 26 | Tags = @('security', 'DFIR', 'defense') 27 | 28 | LicenseUri = 'https://github.com/mattifestation/CatalogTools/blob/master/LICENSE' 29 | 30 | ProjectUri = 'https://github.com/mattifestation/CatalogTools' 31 | 32 | ReleaseNotes = @' 33 | 0.1.0 34 | ----- 35 | Initial release. 36 | 37 | Enhancements: 38 | * Added Get-CatalogFile 39 | '@ 40 | } 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /CatalogTools/CatalogTools.psm1: -------------------------------------------------------------------------------- 1 | Get-ChildItem $PSScriptRoot -Recurse -Exclude 'Lib', 'Tests' -File -Include *.ps1 | 2 | ForEach-Object { . $_.FullName } -------------------------------------------------------------------------------- /CatalogTools/Lib/BouncyCastle.Crypto.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattifestation/CatalogTools/fdad9e905e38214158c801002d57f1feb7480a06/CatalogTools/Lib/BouncyCastle.Crypto.dll -------------------------------------------------------------------------------- /CatalogTools/Parsers/CATParser.ps1: -------------------------------------------------------------------------------- 1 | function Get-CatalogFile { 2 | <# 3 | .SYNOPSIS 4 | 5 | Catalog (.cat) file parser. 6 | 7 | Author: Matthew Graeber (@mattifestation) 8 | License: BSD 3-Clause 9 | 10 | .DESCRIPTION 11 | 12 | Get-CatalogFile parses catalog files without relying upon built-in Win32 APIs. 13 | 14 | .PARAMETER Path 15 | 16 | Specifies the path to one or more catalog files. 17 | 18 | .EXAMPLE 19 | 20 | Get-ChildItem 'C:\Windows\System32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}' | Get-CatalogFile 21 | 22 | .EXAMPLE 23 | 24 | Get-ChildItem C:\ -Recurse -Include *.cat | Get-CatalogFile 25 | 26 | .EXAMPLE 27 | 28 | Get-CatalogFile -Path oem1.cat 29 | 30 | .INPUTS 31 | 32 | System.IO.FileInfo 33 | 34 | Accepts file output from Get-ChildItem or Get-Item. Get-CatalogFile only parses files with the .cat extension. 35 | 36 | .OUTPUTS 37 | 38 | CatalogTools.ParsedCatalog 39 | 40 | Outputs a custom object coinsisting of parsed catalog file data - timestamp, header, members, and signature. 41 | #> 42 | 43 | [CmdletBinding()] 44 | param ( 45 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] 46 | [Alias('FullName')] 47 | [ValidateNotNullOrEmpty()] 48 | [String[]] 49 | $Path 50 | ) 51 | 52 | PROCESS { 53 | foreach ($FilePath in $Path) { 54 | $FullPath = Resolve-Path -Path $FilePath 55 | 56 | $FileInfo = Get-Item -Path $FullPath 57 | 58 | if ($FileInfo.Extension -ne '.cat') { 59 | Write-Error "$FullPath does not have the .cat extension." 60 | continue 61 | } 62 | 63 | $FileStream = [IO.File]::OpenRead($FullPath) 64 | 65 | if (-not $FileStream) { continue } 66 | 67 | $ASN1InputStream = New-Object -TypeName Org.BouncyCastle.Asn1.Asn1InputStream -ArgumentList $FileStream 68 | 69 | $ASN1Object = $ASN1InputStream.ReadObject() 70 | 71 | if (-not $ASN1Object) { 72 | Write-Error "$FullPath is not ASN.1 encoded data." 73 | $ASN1InputStream.Close() 74 | $FileStream.Close() 75 | continue 76 | } 77 | 78 | if (($ASN1Object.Count -lt 2)) { 79 | Write-Error "$($FullPath): ASN.1 encoded data does not hold enough information to hold PKCS#7 ASN.1 SignedData (1.2.840.113549.1.7.2)." 80 | $ASN1InputStream.Close() 81 | $FileStream.Close() 82 | continue 83 | } 84 | 85 | if (-not ($ASN1Object[0] -is [Org.BouncyCastle.Asn1.DerObjectIdentifier])) { 86 | Write-Error "$($FullPath): ASN.1 encoded data is not PKCS#7 ASN.1 SignedData (1.2.840.113549.1.7.2). It must contain an OID datatype." 87 | $ASN1InputStream.Close() 88 | $FileStream.Close() 89 | continue 90 | } 91 | 92 | if ($ASN1Object[0].Id -ne '1.2.840.113549.1.7.2') { 93 | Write-Error "$($FullPath): ASN.1 encoded data is not PKCS#7 ASN.1 SignedData. Its OID must be 1.2.840.113549.1.7.2." 94 | $ASN1InputStream.Close() 95 | $FileStream.Close() 96 | continue 97 | } 98 | 99 | if (-not ($ASN1Object[1] -is [Org.BouncyCastle.Asn1.DerTaggedObject])) { 100 | Write-Error "$($FullPath): ASN.1 encoded data is not PKCS#7 ASN.1 SignedData (1.2.840.113549.1.7.2). It must contain a context-specific tag." 101 | $ASN1InputStream.Close() 102 | $FileStream.Close() 103 | continue 104 | } 105 | 106 | if (($ASN1Object[1].TagNo -ne 0) -or ($ASN1Object[1].IsEmpty())) { 107 | Write-Error "$($FullPath): ASN.1 encoded data is not PKCS#7 ASN.1 SignedData (1.2.840.113549.1.7.2). It must contain a non-empty context-specific tag ([0])." 108 | $ASN1InputStream.Close() 109 | $FileStream.Close() 110 | continue 111 | } 112 | 113 | $SignedDataObject = $ASN1Object[1].GetObject() 114 | 115 | if ((-not $SignedDataObject) -or (-not ($SignedDataObject -is [Org.BouncyCastle.Asn1.DerSequence])) -or ($SignedDataObject.Count -lt 4)) { 116 | Write-Error "$($FullPath): Embedded PKCS#7 ASN.1 SignedData data type must be a SEQUENCE consisting of at least 4 elements." 117 | $ASN1InputStream.Close() 118 | $FileStream.Close() 119 | continue 120 | } 121 | 122 | # PKCS#7 ASN.1 SignedData is defined in RFC2315 123 | 124 | if (-not ($SignedDataObject[0] -is [Org.BouncyCastle.Asn1.DerInteger])) { 125 | Write-Error "$($FullPath): No PKCS#7 ASN.1 SignedData Version field present." 126 | $ASN1InputStream.Close() 127 | $FileStream.Close() 128 | continue 129 | } 130 | 131 | if ($SignedDataObject[0].PositiveValue -ne 1) { 132 | Write-Error "$($FullPath): PKCS#7 ASN.1 SignedData must have a Version of 1. Returned version: $($SignedDataObject[0].PositiveValue)." 133 | $ASN1InputStream.Close() 134 | $FileStream.Close() 135 | continue 136 | } 137 | 138 | # Unless I care at a later time about the signer message digest algorithm, only validate that this required field is present. 139 | # At this time, I only care about catalog information in the ContentInfo property. 140 | if (-not ($SignedDataObject[1] -is [Org.BouncyCastle.Asn1.DerSet])) { 141 | Write-Error "$($FullPath): DigestAlgorithmIdentifiers PKCS#7 ASN.1 SignedData field is not present." 142 | $ASN1InputStream.Close() 143 | $FileStream.Close() 144 | continue 145 | } 146 | 147 | if (-not ($SignedDataObject[2] -is [Org.BouncyCastle.Asn1.DerSequence]) -or ($SignedDataObject[2].Count -ne 2)) { 148 | Write-Error "$($FullPath): ContentInfo PKCS#7 ASN.1 SignedData field must be of type SEQUENCE with two elements." 149 | $ASN1InputStream.Close() 150 | $FileStream.Close() 151 | continue 152 | } 153 | 154 | if (-not ($SignedDataObject[2][0] -is [Org.BouncyCastle.Asn1.DerObjectIdentifier])) { 155 | Write-Error "$($FullPath): ContentInfo PKCS#7 ASN.1 SignedData does not have an embedded OBJECT IDENTIFIER." 156 | $ASN1InputStream.Close() 157 | $FileStream.Close() 158 | continue 159 | } 160 | 161 | # A CTL is a list of hashes of certificates or a list of file names. 162 | # https://msdn.microsoft.com/en-us/library/windows/desktop/ms721572(v=vs.85).aspx#_security_certificate_trust_list_gly 163 | if ($SignedDataObject[2][0].Id -ne '1.3.6.1.4.1.311.10.1') { 164 | Write-Error "$($FullPath): ContentInfo PKCS#7 ASN.1 SignedData is not of type PKCS #7 ContentType Object Identifier for Certificate Trust List (CTL) (1.3.6.1.4.1.311.10.1). OID continueed: $($SignedDataObject[2][0].Id)." 165 | $ASN1InputStream.Close() 166 | $FileStream.Close() 167 | continue 168 | } 169 | 170 | if (-not ($SignedDataObject[2][1] -is [Org.BouncyCastle.Asn1.DerTaggedObject]) -or ($SignedDataObject[2][1].TagNo -ne 0) -or ($SignedDataObject[2][1].IsEmpty()) -or (-not $SignedDataObject[2][1].IsExplicit())) { 171 | Write-Error "$($FullPath): ContentInfo PKCS#7 ASN.1 SignedData does not have an embedded non-empty, explicit context-specific tag." 172 | $ASN1InputStream.Close() 173 | $FileStream.Close() 174 | continue 175 | } 176 | 177 | $CatalogRootObject = $SignedDataObject[2][1].GetObject() 178 | 179 | # Start parsing/validating the catalog file header. 180 | if (-not ($CatalogRootObject -is [Org.BouncyCastle.Asn1.DerSequence]) -and ($CatalogRootObject.Count -lt 5)) { 181 | Write-Error "$($FullPath): Certificate Trust List data must have at least 5 elements for a szOID_CATALOG_LIST type." 182 | $ASN1InputStream.Close() 183 | $FileStream.Close() 184 | continue 185 | } 186 | 187 | if (-not ($CatalogRootObject[0] -is [Org.BouncyCastle.Asn1.DerSequence]) -or ($CatalogRootObject[0].Count -lt 1) -or ($CatalogRootObject[0].Id -ne '1.3.6.1.4.1.311.12.1.1')) { 188 | Write-Error "$($FullPath): Certificate Trust List does not have embedded data of type szOID_CATALOG_LIST (1.3.6.1.4.1.311.12.1.1)." 189 | $ASN1InputStream.Close() 190 | $FileStream.Close() 191 | continue 192 | } 193 | 194 | if (-not ($CatalogRootObject[1] -is [Org.BouncyCastle.Asn1.DerOctetString])) { 195 | Write-Error "$($FullPath): Catalog list does not contain a list identifier." 196 | $ASN1InputStream.Close() 197 | $FileStream.Close() 198 | continue 199 | } 200 | 201 | $ListIdentifier = ($CatalogRootObject[1].GetOctets() | ForEach-Object { "{0:X2}" -f $_ }) -join '' 202 | 203 | if (-not ($CatalogRootObject[2] -is [Org.BouncyCastle.Asn1.DerUtcTime])) { 204 | Write-Error "$($FullPath): Catalog list does not contain a timestamp." 205 | $ASN1InputStream.Close() 206 | $FileStream.Close() 207 | continue 208 | } 209 | 210 | $Timestamp = $CatalogRootObject[2].ToDateTime() 211 | 212 | $CatalogVersion = $null 213 | 214 | switch ($CatalogRootObject[3][0].Id) { 215 | '1.3.6.1.4.1.311.12.1.2' { $CatalogVersion = 1 } 216 | '1.3.6.1.4.1.311.12.1.3' { $CatalogVersion = 2 } 217 | default { 218 | Write-Error "$($FullPath): Undefined catalog version OID. OID returned: $($CatalogRootObject[3][0].Id)" 219 | $ASN1InputStream.Close() 220 | $FileStream.Close() 221 | continue 222 | } 223 | } 224 | 225 | $HeaderAttributeObjects = $null 226 | 227 | # Parse header attributes if they exist. 228 | if ($CatalogRootObject.Count -eq 6) { 229 | if (-not ($CatalogRootObject[5] -is [Org.BouncyCastle.Asn1.DerTaggedObject]) -or ($CatalogRootObject[5].TagNo -ne 0) -or ($CatalogRootObject[5].IsEmpty())) { 230 | Write-Error "$($FullPath): Catalog header attributes are of an incorrect type." 231 | $ASN1InputStream.Close() 232 | $FileStream.Close() 233 | continue 234 | } 235 | 236 | $HeaderAttributes = $CatalogRootObject[5].GetObject() 237 | 238 | if (-not ($HeaderAttributes -is [Org.BouncyCastle.Asn1.DerSequence]) -or ($HeaderAttributes.Count -lt 1)) { 239 | Write-Error "$($FullPath): Catalog header attributes are of an incorrect type." 240 | $ASN1InputStream.Close() 241 | $FileStream.Close() 242 | continue 243 | } 244 | 245 | $HeaderAttributeObjects = New-Object -TypeName PSObject[]($HeaderAttributes.Count) 246 | 247 | for ($i = 0; $i -lt $HeaderAttributes.Count; $i++) { 248 | if (-not ($HeaderAttributes[$i] -is [Org.BouncyCastle.Asn1.DerSequence]) -or ($HeaderAttributes[$i].Count -ne 2)) { 249 | Write-Error "$($FullPath): Catalog header attribute is of an incorrect type." 250 | $ASN1InputStream.Close() 251 | $FileStream.Close() 252 | continue 253 | } 254 | 255 | $OID = $HeaderAttributes[$i][0].Id 256 | 257 | if ($OID -ne '1.3.6.1.4.1.311.12.2.1') { 258 | Write-Warning "$($FullPath): Incorrect catalog header attribute object identifier. OID returned: $OID" 259 | } 260 | 261 | $HeaderAttrProperties = [Org.BouncyCastle.Asn1.Asn1Object]::FromByteArray($HeaderAttributes[$i][1].GetOctets()) 262 | 263 | $AttributeName = $HeaderAttrProperties[0].ToString() 264 | # Depending on how things may be encoded, 265 | # I might not be able to get away with Unicode encoding everything. 266 | $AttributeValue = [Text.Encoding]::Unicode.GetString($HeaderAttrProperties[2].GetOctets()) 267 | 268 | $HeaderAttributeObjects[$i] = [PSCustomObject] @{ 269 | PSTypeName = 'CatalogTools.ParsedCatalog.NameValuePair' 270 | Name = $AttributeName 271 | Value = $AttributeValue 272 | } 273 | } 274 | } 275 | 276 | # Used for parser debugging. This should never be the case from my observations. 277 | if ($CatalogRootObject.Count -gt 6) { 278 | Write-Warning "$($FullPath): Catalog list has more than 6 entries." 279 | } 280 | 281 | # Start parsing the individual members of the catalog file 282 | if (-not ($CatalogRootObject[4] -is [Org.BouncyCastle.Asn1.DerSequence])) { 283 | Write-Error "$($FullPath): Catalog list does not contain a sequence of members." 284 | $ASN1InputStream.Close() 285 | $FileStream.Close() 286 | continue 287 | } 288 | 289 | $MemberSequence = $CatalogRootObject[4] 290 | 291 | $CatalogMembers = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]' 292 | 293 | # At this point, I've decided to lay off extensive ASN.1 validation since I'm confident that 294 | # I'm dealing with a legitimate catalog file. The chances for a malformed file format are 295 | # signficantly reduced at this point. 296 | foreach ($Member in $MemberSequence) { 297 | $TagName = $null 298 | 299 | switch ($CatalogVersion) { 300 | 1 { $TagName = [Text.Encoding]::Unicode.GetString($Member[0].GetOctets()) } 301 | 2 { $TagName = ($Member[0].GetOctets() | ForEach-Object { "{0:X2}" -f $_ }) -join '' } 302 | } 303 | 304 | $NameValuePairs = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]' 305 | $MemberInfo = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]' 306 | $HashInfo = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]' 307 | 308 | foreach ($MemberAttribute in $Member[1]) { 309 | $MemberAttrType = $MemberAttribute[0].Id 310 | 311 | switch ($MemberAttrType) { 312 | '1.3.6.1.4.1.311.12.2.1' { # CAT_NAMEVALUE_OBJID 313 | $AttributeName = $MemberAttribute[1][0][0] 314 | # Depending on how things may be encoded, 315 | # I might not be able to get away with Unicode encoding everything. 316 | $AttributeValue = [Text.Encoding]::Unicode.GetString($MemberAttribute[1][0][2].GetOctets()) 317 | 318 | $Object = [PSCustomObject] @{ 319 | PSTypeName = 'CatalogTools.ParsedCatalog.NameValuePair' 320 | Name = $AttributeName 321 | Value = $AttributeValue 322 | } 323 | 324 | $NameValuePairs.Add($Object) 325 | } 326 | 327 | '1.3.6.1.4.1.311.12.2.2' { # CAT_MEMBERINFO_OBJID 328 | $SubjectGuid = $MemberAttribute[1][0][0].ToString() 329 | $CertificateVersion = $MemberAttribute[1][0][1].Value.IntValue 330 | 331 | $Object = [PSCustomObject] @{ 332 | PSTypeName = 'CatalogTools.ParsedCatalog.MemberCertificateGuid' 333 | SubjectGuid = $SubjectGuid 334 | CertificateVersion = $CertificateVersion 335 | } 336 | 337 | $MemberInfo.Add($Object) 338 | } 339 | 340 | '1.3.6.1.4.1.311.12.2.3' { # CAT_MEMBERINFO2_OBJID 341 | # I have yet to see this populated with actual data 342 | # In the mean time, I will throw a warning if it is populated. 343 | 344 | <# 345 | typedef struct _CAT_MEMBERINFO2 346 | { 347 | GUID SubjectGuid; 348 | DWORD dwCertVersion; 349 | 350 | } CAT_MEMBERINFO2, *PCAT_MEMBERINFO2; 351 | #> 352 | 353 | if ($MemberAttribute[1].GetObject().GetOctets()) { 354 | Write-Warning "$($FullPath): CAT_MEMBERINFO2 struct populated. Inspect the data here and parse accordingly." 355 | } 356 | } 357 | 358 | '1.3.6.1.4.1.311.2.1.4' { # Authenticode - SPC_INDIRECT_DATA_OBJID 359 | $HashAlgorithm = $null 360 | $HashOid = $MemberAttribute[1][0][1][0][0].Id 361 | 362 | switch ($HashOid) { 363 | '2.16.840.1.101.3.4.2.1' { $HashAlgorithm = 'SHA256' } 364 | '1.3.14.3.2.26' { $HashAlgorithm = 'SHA1' } 365 | default { Write-Warning "$($FullPath): Unimplemented algorithm OID: $HashOid" } 366 | } 367 | 368 | $Hash = ($MemberAttribute[1][0][1][1].GetOctets() | ForEach-Object { "{0:X2}" -f $_ }) -join '' 369 | 370 | $AttributeTypeValueData = $MemberAttribute[1][0][0] 371 | $AttributeTypeValueDataOid = $MemberAttribute[1][0][0][0].Id 372 | 373 | switch ($AttributeTypeValueDataOid) { 374 | '1.3.6.1.4.1.311.2.1.15' { # SPC_PE_IMAGE_DATA_OBJID - i.e. most likely, just page hashes 375 | $ImageData = $AttributeTypeValueData[1][1].GetObject() 376 | 377 | $PageHashBytes = $null 378 | $TagGuid = $null 379 | $EnhancedHash = $null 380 | 381 | if ($ImageData.TagNo -eq 2) { 382 | $PageHashAttr = $ImageData.GetObject().GetObject() 383 | 384 | if ($PageHashAttr) { 385 | $PageHashAttrBytes = $PageHashAttr.GetOctets() 386 | 387 | if ($PageHashAttrBytes) { 388 | $ObsoletePageHashTag = [Text.Encoding]::ASCII.GetString(($PageHashAttr.GetOctets() | Where-Object { $_ })) 389 | 390 | if ($ObsoletePageHashTag -ne '<<>>') { 391 | Write-Warning "$($FullPath): Unsupported page hash type!" 392 | } 393 | } 394 | } 395 | } else { 396 | $TagGuid = [Guid] $AttributeTypeValueData[1][1].GetObject().GetObject()[0].GetOctets() 397 | 398 | $PageHashData = [Org.BouncyCastle.Asn1.Asn1Object]::FromByteArray($AttributeTypeValueData[1][1].GetObject().GetObject()[1].GetOctets()) 399 | $PageHashVersionOid = $PageHashData[0][0].Id 400 | 401 | switch ($PageHashVersionOid) { 402 | '1.3.6.1.4.1.311.2.3.1' { # SPC_PE_IMAGE_PAGE_HASHES_V1_OBJID 403 | $PageHashVersion = 1 404 | } 405 | 406 | '1.3.6.1.4.1.311.2.3.2' { # SPC_PE_IMAGE_PAGE_HASHES_V2_OBJID 407 | $PageHashVersion = 2 408 | } 409 | 410 | default { 411 | $PageHashVersion = $null 412 | Write-Warning "$($FullPath): Unsupported SPC_PE_IMAGE_PAGE_HASHES type: $PageHashVersionOid" 413 | } 414 | } 415 | 416 | [Byte[]] $PageHashBytes = $PageHashData[0][1].GetOctets() 417 | } 418 | } 419 | 420 | '1.3.6.1.4.1.311.2.1.25' { # SPC_GLUE_RDN_OBJID or SPC_CAB_DATA_OBJID 421 | # I've only ever seen this as '<<>>' 422 | $GlueObject = $AttributeTypeValueData[1].GetObject().GetObject() 423 | $GlueTag = $null 424 | 425 | # This type may be present but it will probably be null. 426 | if ($GlueObject) { 427 | $GlueObjectBytes = $GlueObject.GetOctets() 428 | 429 | if ($GlueObjectBytes) { 430 | $GlueTag = [Text.Encoding]::ASCII.GetString(($GlueObject.GetOctets() | Where-Object { $_ })) 431 | 432 | if ($GlueTag -ne '<<>>') { 433 | Write-Warning "$($FullPath): Unsupported `"glue tag`" data (1.3.6.1.4.1.311.2.1.25): $GlueTag. Try parsing SPC_CAB_DATA_OBJID data." 434 | } 435 | } 436 | } 437 | } 438 | 439 | '1.3.6.1.4.1.311.2.1.28' { # SPC_LINK_OBJID 440 | $TagGuid = [Guid] $AttributeTypeValueData[1].GetObject()[0].GetOctets() 441 | $EnhancedHash = ($AttributeTypeValueData[1].GetObject()[1].GetOctets() | ForEach-Object { "{0:X2}" -f $_ }) -join '' 442 | } 443 | 444 | default { 445 | Write-Warning "$($FullPath): Unsupported SPC_INDIRECT_DATA_CONTENT type OID: $AttributeTypeValueDataOid. $FullPath" 446 | } 447 | } 448 | 449 | $Object = [PSCustomObject] @{ 450 | PSTypeName = 'CatalogTools.ParsedCatalog.MemberHashInformation' 451 | Algorithm = $HashAlgorithm 452 | FileHash = $Hash 453 | Guid = $TagGuid 454 | PageHashVersion = $PageHashVersion 455 | PageHashData = $PageHashBytes 456 | EnhancedHash = $EnhancedHash 457 | } 458 | 459 | $HashInfo.Add($Object) 460 | } 461 | 462 | default { 463 | Write-Warning "$($FullPath): Unsupported list member attribute type: $MemberAttrType" 464 | } 465 | } 466 | } 467 | 468 | $Object = [PSCustomObject] @{ 469 | Tag = $TagName 470 | HashInfo = if ($HashInfo.Count) { $HashInfo } else { $null } 471 | NameValuePairs = if ($NameValuePairs.Count) { $NameValuePairs } else { $null } 472 | } 473 | 474 | $CatalogMembers.Add($Object) 475 | } 476 | 477 | $ParsedCatalog = [PSCustomObject] @{ 478 | PSTypeName = 'CatalogTools.ParsedCatalog' 479 | FilePath = $FullPath 480 | ListIdentifier = $ListIdentifier 481 | CatalogVersion = $CatalogVersion 482 | EffectiveDate = $Timestamp 483 | HeaderAttributes = $HeaderAttributeObjects 484 | CatalogMembers = $CatalogMembers 485 | Signer = (Get-AuthenticodeSignature -FilePath $FullPath) 486 | } 487 | 488 | $ParsedCatalog 489 | 490 | $ASN1InputStream.Close() 491 | $FileStream.Close() 492 | } 493 | } 494 | } 495 | 496 | Export-ModuleMember -Function 'Get-CatalogFile' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Matt Graeber 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /LICENSE_Bouncy_Castles: -------------------------------------------------------------------------------- 1 | Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CatalogTools 2 | A PowerShell module to assist in parsing and managing catalog files. 3 | 4 | ### Additional license information 5 | CatalogTools relies upon the Bouncy Castle C# library for ASN.1 decoding. See LICENSE_Bouncy_Castles for license terms. 6 | --------------------------------------------------------------------------------