reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
200
reverse-skill-repo/skills/scripts/append-evidence.ps1
Normal file
200
reverse-skill-repo/skills/scripts/append-evidence.ps1
Normal file
@@ -0,0 +1,200 @@
|
||||
#Requires -Version 5.1
|
||||
# Append one Evidence item under work/<case>/evidence/ (ops Evidence contract).
|
||||
#
|
||||
# Usage (simple):
|
||||
# powershell -File skills/scripts/append-evidence.ps1 -CaseRoot work\my-case `
|
||||
# -Id E-001 -Title "Open clock API" -ReproCommand 'curl -sI https://example/' `
|
||||
# -Severity info -Status observed
|
||||
#
|
||||
# Special characters / spaces / quotes in excerpts (recommended for nested -File):
|
||||
# Set-Content excerpt.txt -Value '"XML parsing error" / Entities are not allowed'
|
||||
# powershell -File skills/scripts/append-evidence.ps1 ... -RawExcerptFile excerpt.txt
|
||||
#
|
||||
# NOTE: Nested `powershell -File` splits unquoted multi-word values. Prefer -RawExcerptFile
|
||||
# or call the script in-process: & .\append-evidence.ps1 -RawExcerpt 'full text here'
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $CaseRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Id,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Title,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $ReproCommand,
|
||||
|
||||
[string] $Severity = 'info',
|
||||
[string] $Status = 'observed',
|
||||
[string] $SourceType = 'command',
|
||||
[string] $RawExcerpt = '',
|
||||
[string] $RawExcerptFile = '',
|
||||
[string] $Location = '',
|
||||
[string] $Notes = '',
|
||||
[string] $NotesFile = '',
|
||||
[string] $ReproCommandFile = '',
|
||||
[string] $ArtifactPath = ''
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Advanced functions reject unknown positionals at bind time; keep explicit check for edge hosts.
|
||||
if ($args -and $args.Count -gt 0) {
|
||||
$extra = ($args | ForEach-Object { [string]$_ }) -join ' | '
|
||||
throw ("Unexpected arguments (likely -RawExcerpt/-Title quoting broke multi-word value): {0}. Use -RawExcerptFile for special characters, or pass a single quoted string." -f $extra)
|
||||
}
|
||||
|
||||
$allowedSeverity = @('critical', 'high', 'medium', 'low', 'info', 'n/a', 'n/a_re')
|
||||
$allowedStatus = @('observed', 'candidate', 'validated', 'false_positive', 'accepted_risk')
|
||||
|
||||
function ConvertTo-YamlBlock([string] $text) {
|
||||
if ([string]::IsNullOrEmpty($text)) { return ' n/a' }
|
||||
$clean = ($text -replace '[\x00-\x08\x0B\x0C\x0E-\x1F]', '')
|
||||
$lines = $clean -split "`r?`n", -1
|
||||
if ($lines.Count -eq 0) { return ' n/a' }
|
||||
return (($lines | ForEach-Object { ' ' + $_ }) -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
function ConvertTo-SingleLine([string] $text) {
|
||||
if ([string]::IsNullOrEmpty($text)) { return 'n/a' }
|
||||
$t = ($text -replace '[\x00-\x1F]', ' ')
|
||||
$t = $t -replace '\s+', ' '
|
||||
return $t.Trim()
|
||||
}
|
||||
|
||||
function Read-OptionalFile([string] $path) {
|
||||
if ([string]::IsNullOrWhiteSpace($path)) { return $null }
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
throw "File not found: $path"
|
||||
}
|
||||
return [System.IO.File]::ReadAllText((Resolve-Path -LiteralPath $path), [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $CaseRoot)) {
|
||||
throw "CaseRoot does not exist: $CaseRoot"
|
||||
}
|
||||
|
||||
# File overrides beat inline strings (safe path for special characters under powershell -File)
|
||||
$fromReproFile = Read-OptionalFile $ReproCommandFile
|
||||
if ($null -ne $fromReproFile) { $ReproCommand = $fromReproFile }
|
||||
|
||||
$fromExcerptFile = Read-OptionalFile $RawExcerptFile
|
||||
if ($null -ne $fromExcerptFile) { $RawExcerpt = $fromExcerptFile }
|
||||
|
||||
$fromNotesFile = Read-OptionalFile $NotesFile
|
||||
if ($null -ne $fromNotesFile) { $Notes = $fromNotesFile }
|
||||
|
||||
$contentHash = 'n/a'
|
||||
$artifactRef = 'n/a'
|
||||
if (-not [string]::IsNullOrWhiteSpace($ArtifactPath)) {
|
||||
$artifactCandidate = $ArtifactPath
|
||||
if (-not [System.IO.Path]::IsPathRooted($artifactCandidate)) {
|
||||
$caseArtifact = Join-Path $CaseRoot $artifactCandidate
|
||||
if (Test-Path -LiteralPath $caseArtifact -PathType Leaf) { $artifactCandidate = $caseArtifact }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $artifactCandidate -PathType Leaf)) {
|
||||
throw "ArtifactPath must point to a file inside Caseroot:REDACTED"
|
||||
}
|
||||
$caseRootFull = (Resolve-Path -LiteralPath $CaseRoot).Path
|
||||
$artifactFull = (Resolve-Path -LiteralPath $artifactCandidate).Path
|
||||
$casePrefix = $caseRootFull.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
|
||||
if (-not $artifactFull.StartsWith($casePrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "ArtifactPath must point inside Caseroot:REDACTED"
|
||||
}
|
||||
$artifactRef = $artifactFull.Substring($casePrefix.Length) -replace '\\', '/'
|
||||
$contentHash = 'sha256:' + (Get-FileHash -LiteralPath $artifactFull -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
$evDir = Join-Path $CaseRoot 'evidence'
|
||||
New-Item -ItemType Directory -Force -Path $evDir | Out-Null
|
||||
|
||||
$idSafe = ($Id -replace '[^\w\-]+', '-').Trim('-')
|
||||
if ($idSafe -notmatch '^E-') { $idSafe = 'E-' + $idSafe }
|
||||
$fileName = $idSafe + '.md'
|
||||
$path = Join-Path $evDir $fileName
|
||||
|
||||
$observed = Get-Date -Format 'o'
|
||||
$titleLine = ConvertTo-SingleLine $Title
|
||||
$sev = (ConvertTo-SingleLine $Severity).ToLowerInvariant()
|
||||
if ($allowedSeverity -notcontains $sev) {
|
||||
throw ("Invalid -Severity '{0}' (allowed: {1})" -f $Severity, ($allowedSeverity -join ', '))
|
||||
}
|
||||
$st = (ConvertTo-SingleLine $Status).ToLowerInvariant()
|
||||
if ($allowedStatus -notcontains $st) {
|
||||
throw ("Invalid -Status '{0}' (allowed: {1})" -f $Status, ($allowedStatus -join ', '))
|
||||
}
|
||||
$src = ConvertTo-SingleLine $SourceType
|
||||
$loc = ConvertTo-SingleLine $(if ($Location) { $Location } else { 'n/a' })
|
||||
if ([string]::IsNullOrWhiteSpace($ReproCommand)) {
|
||||
throw '-ReproCommand (or -ReproCommandFile) is required and must be non-empty'
|
||||
}
|
||||
$reproBlock = ConvertTo-YamlBlock $ReproCommand
|
||||
$excerptBlock = ConvertTo-YamlBlock $(if ($RawExcerpt) { $RawExcerpt } else { 'n/a' })
|
||||
$notesBlock = ConvertTo-YamlBlock $(if ($Notes) { $Notes } else { 'n/a' })
|
||||
|
||||
$body = @"
|
||||
### $idSafe
|
||||
- title: $titleLine
|
||||
- observed_at: $observed
|
||||
- source_type: $src
|
||||
- source_ref: append-evidence.ps1
|
||||
- content_hash: $contentHash
|
||||
- artifact_path: $artifactRef
|
||||
- severity: $sev
|
||||
- status: $st
|
||||
- location: $loc
|
||||
- repro_command: |
|
||||
$reproBlock
|
||||
- raw_excerpt: |
|
||||
$excerptBlock
|
||||
- linked_workitem: n/a
|
||||
- supersedes: none
|
||||
- notes: |
|
||||
$notesBlock
|
||||
"@
|
||||
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
$stream = $null
|
||||
$writer = $null
|
||||
try {
|
||||
try {
|
||||
$stream = [System.IO.File]::Open(
|
||||
$path,
|
||||
[System.IO.FileMode]::CreateNew,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::None
|
||||
)
|
||||
} catch [System.IO.IOException] {
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
throw ("Evidence record already exists and is immutable: {0}. Use a new -Id." -f $fileName)
|
||||
}
|
||||
throw
|
||||
}
|
||||
$writer = [System.IO.StreamWriter]::new($stream, $utf8)
|
||||
$writer.Write($body)
|
||||
} finally {
|
||||
if ($null -ne $writer) {
|
||||
$writer.Dispose()
|
||||
} elseif ($null -ne $stream) {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$index = Join-Path $evDir 'INDEX.md'
|
||||
$line = "- $idSafe | $sev | $st | $titleLine | $fileName"
|
||||
if (-not (Test-Path -LiteralPath $index)) {
|
||||
$hdr = @"
|
||||
# Evidence index
|
||||
|
||||
| id | severity | status | title | file |
|
||||
|----|----------|--------|-------|------|
|
||||
|
||||
"@
|
||||
[System.IO.File]::WriteAllText($index, $hdr, $utf8)
|
||||
}
|
||||
Add-Content -Path $index -Value $line -Encoding UTF8
|
||||
|
||||
Write-Host ("EVIDENCE -> {0}" -f $path) -ForegroundColor Green
|
||||
Write-Host ("ID: {0} | {1} | {2}" -f $idSafe, $sev, $st)
|
||||
exit 0
|
||||
Reference in New Issue
Block a user