Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947bb8e020 | ||
|
|
a9432324a7 | ||
|
|
cc274f39a8 | ||
|
|
44e9bd2916 |
@@ -682,6 +682,7 @@ jobs:
|
||||
run: >-
|
||||
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass
|
||||
-File scripts\windows-verify-signatures.ps1
|
||||
app\src-tauri\target\release\triple-c.exe
|
||||
app\src-tauri\target\release\bundle\msi\*.msi
|
||||
app\src-tauri\target\release\bundle\nsis\*.exe
|
||||
|
||||
|
||||
@@ -857,20 +857,14 @@ merge, dispatch `build-app.yml` on the branch. Every publishing step there is ga
|
||||
- `scripts/windows-sign.ps1` is the sign command: `signtool sign /dlib` with SHA-256 and the
|
||||
Microsoft timestamp server, retried. Credentials never reach a command line — the dlib reads
|
||||
`AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` from the environment.
|
||||
**It signs only an allowlist of what ships**: 5 signatures per release (the app binary twice,
|
||||
because Tauri re-patches it between the MSI and NSIS bundles; the MSI; the NSIS installer;
|
||||
and its uninstaller). Tauri also presents build-time tools, the WiX extension DLLs and NSIS
|
||||
plugins, and signing those would more than double the metered count for no user-visible
|
||||
benefit. If the app ever ships resource DLLs or sidecars, extend the
|
||||
**It signs only an allowlist of what ships**, about 4 signatures per release. Tauri also
|
||||
presents build-time tools: the WiX extension DLLs, the NSIS plugins, and the app binary a
|
||||
second time for the second bundle type. Signing those would roughly triple the metered count
|
||||
for no user-visible benefit. If the app ever ships resource DLLs or sidecars, extend the
|
||||
allowlist, or they will go out unsigned. Tauri reports a failed sign command only as
|
||||
"failed to run powershell", so the script keeps a transcript (`.code-signing/sign-output.log`,
|
||||
`signtool /debug` included), and the job prints it on failure.
|
||||
- `scripts/windows-verify-signatures.ps1` checks `signtool verify /pa` plus a timestamp on the
|
||||
installers, and on the binaries **inside** the MSI (an administrative `msiexec /a` extract).
|
||||
It deliberately does not check `target\release\triple-c.exe`: Tauri patches that file again
|
||||
after packaging, so the loose copy is unsigned by design and is not what ships. For the NSIS
|
||||
installer, which cannot be unpacked that way, it requires the signing log to show the app
|
||||
binary and the uninstaller were signed.
|
||||
- `scripts/windows-verify-signatures.ps1` checks `signtool verify /pa` plus a timestamp.
|
||||
|
||||
Secrets (repository): the three `AZURE_*` above plus `ARTIFACT_SIGNING_ENDPOINT`,
|
||||
`ARTIFACT_SIGNING_ACCOUNT_NAME`, `ARTIFACT_SIGNING_PROFILE_NAME`. They are referenced only by the
|
||||
|
||||
+4
-12
@@ -469,7 +469,6 @@ replaces the old Full Permissions on/off switch.
|
||||
| **Plan** | Proposes a plan and makes no changes | `--permission-mode plan` |
|
||||
| **Default** | Asks before each tool call | *(nothing — Claude Code's own default)* |
|
||||
| **Accept Edits** | Auto-approves file edits; other tools still prompt | `--permission-mode acceptEdits` |
|
||||
| **Auto** | A safety classifier approves routine actions and blocks risky ones, without prompting | `--permission-mode auto` |
|
||||
| **Bypass** | Auto-approves every tool call | `--dangerously-skip-permissions` |
|
||||
|
||||
New projects start in **Default**. Projects created before permission modes existed keep behaving
|
||||
@@ -481,17 +480,12 @@ the way they did: one that had Full Permissions on becomes **Bypass**, one that
|
||||
> has Docker socket access or reaches services on your network. The Overview tab tells you whether
|
||||
> the in-container sandbox is also on.
|
||||
|
||||
**Auto** sits between Accept Edits and Bypass: Claude Code's own classifier reviews each action,
|
||||
lets routine work through and blocks things that look risky (such as destructive or
|
||||
exfiltrating commands) — no prompts either way. Whether it is available depends on your Claude
|
||||
Code account and model; see Claude Code's documentation on permission modes.
|
||||
|
||||
### When a change takes effect
|
||||
|
||||
- **Terminals** — the mode is applied when a terminal is opened, so it affects terminals you open
|
||||
from then on. A Claude session that is already running keeps the permissions it started with;
|
||||
close the tab and open a new terminal to change it. The badge on each terminal tab shows the mode
|
||||
that terminal was launched with (`plan`, `ask`, `edits`, `auto`, `bypass`).
|
||||
that terminal was launched with (`plan`, `ask`, `edits`, `bypass`).
|
||||
- **Resumed sessions** — a session resumed from the **Sessions** tab uses the project's current
|
||||
mode.
|
||||
- **Scheduled tasks** — these now honour the permission mode too (they previously always ran with
|
||||
@@ -500,9 +494,8 @@ Code account and model; see Claude Code's documentation on permission modes.
|
||||
mode change to reach the scheduler.
|
||||
|
||||
> Scheduled tasks run headless (`claude -p`) and cannot answer a permission prompt. In any mode
|
||||
> other than **Auto** or **Bypass**, a task may simply stop early when Claude Code asks for
|
||||
> approval. In **Auto**, actions the classifier blocks are denied and the run carries on without
|
||||
> them. Its run log records which mode it used.
|
||||
> other than **Bypass**, a task may simply stop early when Claude Code asks for approval. Its run
|
||||
> log records which mode it used.
|
||||
|
||||
---
|
||||
|
||||
@@ -1356,8 +1349,7 @@ Scheduled runs use the project's [permission mode](#permission-modes) — they n
|
||||
with `--dangerously-skip-permissions`. Because the mode travels into the container as an
|
||||
environment variable, **stop and start the project** after changing it for the scheduler to see the
|
||||
change. Remember that a headless run cannot answer a permission prompt, so in any mode other than
|
||||
**Auto** or **Bypass** a task may stop early when Claude Code asks for approval (in Auto, blocked
|
||||
actions are denied instead); the run log records the mode
|
||||
**Bypass** a task may stop early when Claude Code asks for approval; the run log records the mode
|
||||
that was used.
|
||||
|
||||
### Creating Tasks
|
||||
|
||||
@@ -114,7 +114,7 @@ progress modal.
|
||||
|
||||
## Permission Modes
|
||||
|
||||
`PermissionMode` in `models/project.rs` replaces the old `full_permissions` boolean. Five states,
|
||||
`PermissionMode` in `models/project.rs` replaces the old `full_permissions` boolean. Four states,
|
||||
mapped to CLI flags by `PermissionMode::cli_args()`:
|
||||
|
||||
| Mode | Serialized | CLI args passed to `claude` |
|
||||
@@ -122,7 +122,6 @@ mapped to CLI flags by `PermissionMode::cli_args()`:
|
||||
| **Plan** | `plan` | `--permission-mode plan` |
|
||||
| **Default** | `default` | *(none)* |
|
||||
| **Accept Edits** | `acceptEdits` | `--permission-mode acceptEdits` |
|
||||
| **Auto** | `auto` | `--permission-mode auto` |
|
||||
| **Bypass** | `bypass` | `--dangerously-skip-permissions` |
|
||||
|
||||
`Project.permission_mode` is `Option<PermissionMode>`; `effective_permission_mode()` falls back to
|
||||
@@ -532,7 +531,7 @@ Triple-C includes optional speech-to-text powered by [Faster Whisper](https://gi
|
||||
| `app/src/components/layout/StatusBar.tsx` | Project/terminal counts, Notes toggle, STT mic |
|
||||
| `app/src/components/projects/ProjectRow.tsx` | Select-only sidebar row; opens Project Home, with hover start/stop and terminal controls |
|
||||
| `app/src/components/projects/ProjectList.tsx` | Project list in sidebar |
|
||||
| `app/src/components/projects/PermissionModeControl.tsx` | Plan / Default / Accept Edits / Auto / Bypass segmented control |
|
||||
| `app/src/components/projects/PermissionModeControl.tsx` | Plan / Default / Accept Edits / Bypass segmented control |
|
||||
| `app/src/components/ui/` | Shared primitives: `Modal`, `Button`, `Toggle`, `Field`, `SegmentedControl`, `StatusIndicator`, `SaveIndicator`, `OverflowMenu`, `ToastHost`, `Tooltip` |
|
||||
| `app/src/hooks/useKeyboardShortcuts.ts` | `Ctrl+T`, `Ctrl+Shift+W`, `Ctrl+Tab`, `Ctrl+1..9`, `Ctrl+Shift+←/→` |
|
||||
| `app/src/hooks/useContainerProgress.ts` | `container-progress` event → inline progress lines |
|
||||
|
||||
+2
-3
@@ -186,7 +186,7 @@ host keychain secrets.
|
||||
|
||||
### Permission Modes
|
||||
|
||||
`PermissionMode` (`models/project.rs`) is a five-state enum replacing the earlier `full_permissions`
|
||||
`PermissionMode` (`models/project.rs`) is a four-state enum replacing the earlier `full_permissions`
|
||||
boolean. It reaches Claude Code by two different routes:
|
||||
|
||||
| Mode | `cli_args()` — interactive terminals | `as_env_value()` — scheduler |
|
||||
@@ -194,7 +194,6 @@ boolean. It reaches Claude Code by two different routes:
|
||||
| `Plan` | `--permission-mode plan` | `plan` |
|
||||
| `Default` | *(no flag)* | `default` |
|
||||
| `AcceptEdits` | `--permission-mode acceptEdits` | `acceptEdits` |
|
||||
| `Auto` | `--permission-mode auto` | `auto` |
|
||||
| `Bypass` | `--dangerously-skip-permissions` | `bypass` |
|
||||
|
||||
`Project.permission_mode` is `Option<PermissionMode>`, and `effective_permission_mode()` resolves
|
||||
@@ -475,7 +474,7 @@ triple-c/
|
||||
│ │ ├── ProjectRow.tsx # Select-only sidebar row
|
||||
│ │ ├── ProjectList.tsx # Sidebar project list
|
||||
│ │ ├── AddProjectDialog.tsx # New-project dialog
|
||||
│ │ ├── PermissionModeControl.tsx # Plan/Default/Accept Edits/Auto/Bypass
|
||||
│ │ ├── PermissionModeControl.tsx # Plan/Default/Accept Edits/Bypass
|
||||
│ │ ├── ConfirmRemoveModal.tsx # Project removal confirmation
|
||||
│ │ └── *Editor.tsx / *Modal.tsx # EnvVars, PortMappings,
|
||||
│ │ # ClaudeInstructions, ClaudeCodeSettings —
|
||||
|
||||
@@ -469,20 +469,6 @@ mod tests {
|
||||
assert!(!cmd[2].contains(" -n "), "empty name must add no flag: {}", cmd[2]);
|
||||
}
|
||||
|
||||
/// Auto mode is passed as a `--permission-mode` value, not its own flag.
|
||||
#[test]
|
||||
fn build_terminal_cmd_passes_auto_permission_mode() {
|
||||
let mut p = project("anthropic", serde_json::Value::Null);
|
||||
p.permission_mode = Some(crate::models::project::PermissionMode::Auto);
|
||||
let cmd = build_claude_terminal_cmd(&p, None, None);
|
||||
|
||||
assert!(
|
||||
cmd[2].contains("exec claude '--permission-mode' 'auto'"),
|
||||
"got: {}",
|
||||
cmd[2]
|
||||
);
|
||||
}
|
||||
|
||||
/// The Bedrock-profile path keeps its AWS validation *and* gains the
|
||||
/// prelude, immediately before the exec.
|
||||
#[test]
|
||||
|
||||
@@ -166,9 +166,6 @@ pub enum PermissionMode {
|
||||
Default,
|
||||
/// Auto-accept file edits, prompt for everything else.
|
||||
AcceptEdits,
|
||||
/// Claude Code's classifier approves safe actions and blocks risky ones,
|
||||
/// without prompting.
|
||||
Auto,
|
||||
/// Skip all permission prompts.
|
||||
Bypass,
|
||||
}
|
||||
@@ -183,7 +180,6 @@ impl PermissionMode {
|
||||
PermissionMode::AcceptEdits => {
|
||||
vec!["--permission-mode".to_string(), "acceptEdits".to_string()]
|
||||
}
|
||||
PermissionMode::Auto => vec!["--permission-mode".to_string(), "auto".to_string()],
|
||||
PermissionMode::Bypass => vec!["--dangerously-skip-permissions".to_string()],
|
||||
}
|
||||
}
|
||||
@@ -195,7 +191,6 @@ impl PermissionMode {
|
||||
PermissionMode::Plan => "plan",
|
||||
PermissionMode::Default => "default",
|
||||
PermissionMode::AcceptEdits => "acceptEdits",
|
||||
PermissionMode::Auto => "auto",
|
||||
PermissionMode::Bypass => "bypass",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ const MODE_BADGE: Record<PermissionMode, { text: string; className: string }> =
|
||||
plan: { text: "plan", className: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)]" },
|
||||
default: { text: "ask", className: "bg-[var(--bg-tertiary)] text-[var(--text-secondary)]" },
|
||||
acceptEdits: { text: "edits", className: "bg-[var(--accent-muted)] text-[var(--accent)]" },
|
||||
auto: { text: "auto", className: "bg-[var(--accent-muted)] text-[var(--accent)]" },
|
||||
bypass: { text: "bypass", className: "bg-[var(--warning-muted)] text-[var(--warning)]" },
|
||||
};
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ describe("PermissionModeControl", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders all five modes as a radio group with the effective one checked", () => {
|
||||
it("renders all four modes as a radio group with the effective one checked", () => {
|
||||
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
||||
const group = screen.getByRole("radiogroup", { name: "Permission mode" });
|
||||
expect(group).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("radio")).toHaveLength(5);
|
||||
expect(screen.getAllByRole("radio")).toHaveLength(4);
|
||||
expect(screen.getByRole("radio", { name: "Default" })).toHaveAttribute(
|
||||
"aria-checked",
|
||||
"true",
|
||||
@@ -92,14 +92,6 @@ describe("PermissionModeControl", () => {
|
||||
expect(onChange).toHaveBeenCalledWith("acceptEdits");
|
||||
});
|
||||
|
||||
it("offers Auto between Accept Edits and Bypass", () => {
|
||||
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
||||
const labels = screen.getAllByRole("radio").map((r) => r.textContent);
|
||||
expect(labels).toEqual(["Plan", "Default", "Accept Edits", "Auto", "Bypass"]);
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Auto" }));
|
||||
expect(onChange).toHaveBeenCalledWith("auto");
|
||||
});
|
||||
|
||||
it("moves selection with the arrow keys", () => {
|
||||
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
||||
fireEvent.keyDown(screen.getByRole("radiogroup", { name: "Permission mode" }), {
|
||||
|
||||
@@ -9,11 +9,6 @@ export const PERMISSION_MODES: Segment<PermissionMode>[] = [
|
||||
label: "Accept Edits",
|
||||
hint: "File edits are auto-approved; other tools still prompt.",
|
||||
},
|
||||
{
|
||||
value: "auto",
|
||||
label: "Auto",
|
||||
hint: "A safety classifier approves routine actions and blocks risky ones, without prompting.",
|
||||
},
|
||||
{
|
||||
value: "bypass",
|
||||
label: "Bypass",
|
||||
|
||||
@@ -159,18 +159,12 @@ describe("TaskEditorModal", () => {
|
||||
});
|
||||
|
||||
it("warns that a headless run cannot answer a permission prompt", async () => {
|
||||
// Bypass (and Auto, below) are the modes where an unattended run cannot stall.
|
||||
// Bypass is the only mode where an unattended run is safe from stalling.
|
||||
await renderEditor(null, { ...baseProject, permission_mode: "bypass" });
|
||||
expect(screen.getByText(/headless/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
|
||||
});
|
||||
|
||||
it("tells Auto mode that blocked actions are denied, not prompted", async () => {
|
||||
await renderEditor(null, { ...baseProject, permission_mode: "auto" });
|
||||
expect(screen.queryByText(/cannot answer a permission prompt/i)).toBeNull();
|
||||
expect(screen.getByText(/blocks are denied/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("spells out the stall risk in any non-Bypass mode", async () => {
|
||||
await renderEditor(null, { ...baseProject, permission_mode: "default" });
|
||||
expect(screen.getByText(/cannot answer a permission prompt/i)).toBeInTheDocument();
|
||||
|
||||
@@ -301,17 +301,11 @@ export default function TaskEditorModal({ project, task, onClose, onSaved }: Pro
|
||||
terminal attached, using this project’s permission mode (
|
||||
<strong className="text-[var(--text-primary)]">{modeLabel}</strong>).
|
||||
</p>
|
||||
{mode === "auto" && (
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
In Auto mode nothing prompts: actions the safety classifier blocks are denied and the
|
||||
run carries on without them, so check the log if a task seems to have skipped a step.
|
||||
</p>
|
||||
)}
|
||||
{mode !== "bypass" && mode !== "auto" && (
|
||||
{mode !== "bypass" && (
|
||||
<p className="text-xs text-[var(--warning)]">
|
||||
A headless run cannot answer a permission prompt. In {modeLabel} mode the task may
|
||||
stall and produce an empty log; set the mode to Auto or Bypass in the Config tab
|
||||
for unattended runs.
|
||||
stall and produce an empty log; set the mode to Bypass in the Config tab for
|
||||
unattended runs.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -126,7 +126,7 @@ export const CUSTOM_ENDPOINT_BACKENDS: readonly Backend[] = [
|
||||
];
|
||||
|
||||
/** Mirrors Rust `PermissionMode` (serde camelCase). */
|
||||
export type PermissionMode = "plan" | "default" | "acceptEdits" | "auto" | "bypass";
|
||||
export type PermissionMode = "plan" | "default" | "acceptEdits" | "bypass";
|
||||
|
||||
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
|
||||
|
||||
|
||||
@@ -61,14 +61,12 @@ TASK_TYPE=$(jq -r '.type' "$TASK_FILE")
|
||||
# project's permission setting. Keep this mapping in sync with
|
||||
# PermissionMode::cli_args() in app/src-tauri/src/models/project.rs.
|
||||
# NOTE: headless `claude -p` runs cannot answer a permission prompt, so any
|
||||
# mode other than "bypass" or "auto" means the task may stop early when Claude
|
||||
# Code asks for permission. In "auto", actions the classifier blocks are
|
||||
# denied rather than prompted, so the run continues without them. Unset or unrecognized values pass no flag (Claude's default).
|
||||
# mode other than "bypass" means the task may stop early when Claude Code asks
|
||||
# for permission. Unset or unrecognized values pass no flag (Claude's default).
|
||||
PERMISSION_ARGS=()
|
||||
case "${TRIPLE_C_PERMISSION_MODE:-}" in
|
||||
plan) PERMISSION_ARGS=(--permission-mode plan) ;;
|
||||
acceptEdits) PERMISSION_ARGS=(--permission-mode acceptEdits) ;;
|
||||
auto) PERMISSION_ARGS=(--permission-mode auto) ;;
|
||||
bypass) PERMISSION_ARGS=(--dangerously-skip-permissions) ;;
|
||||
*) PERMISSION_ARGS=() ;;
|
||||
esac
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
# windows-verify-signatures.ps1 <path-or-wildcard>... - fail unless everything
|
||||
# that ships carries a valid, timestamped Authenticode signature.
|
||||
# windows-verify-signatures.ps1 <path-or-wildcard>... - fail unless every file
|
||||
# carries a valid, timestamped Authenticode signature.
|
||||
#
|
||||
# The check that makes signing load-bearing rather than hopeful: Tauri skips
|
||||
# signing silently in some configurations (no sign command, --no-sign), and an
|
||||
# unsigned installer looks exactly like a signed one until SmartScreen blocks
|
||||
# it on a user's machine. Every pattern must match at least one file, so a
|
||||
# bundle that was never produced cannot pass either.
|
||||
#
|
||||
# Pass the installers, not target\release\triple-c.exe. The app binary users
|
||||
# get is the copy inside each installer: Tauri patches the loose file with
|
||||
# bundle-type information before each bundle, signs it, packages it, and
|
||||
# patches it again, so the loose copy ends up unsigned by design. The MSI is
|
||||
# unpacked with an administrative install and its binaries checked directly;
|
||||
# the NSIS installer cannot be unpacked that way, so for it the signing log
|
||||
# must show the app binary and the uninstaller were signed.
|
||||
|
||||
param([Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)][string[]]$Patterns)
|
||||
|
||||
@@ -26,82 +18,49 @@ $files = foreach ($pattern in $Patterns) {
|
||||
$found
|
||||
}
|
||||
|
||||
function Test-Signature([IO.FileInfo]$File, [string]$Label) {
|
||||
# signtool's own check - chain to a trusted root under the default
|
||||
# Authenticode policy - with Stop relaxed for the native call, as in
|
||||
# windows-sign.ps1.
|
||||
$failed = @()
|
||||
foreach ($file in $files) {
|
||||
# signtool's own check: chain to a trusted root under the default
|
||||
# Authenticode policy.
|
||||
# Stop relaxed for the native call, as in windows-sign.ps1.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$output = & $env:TRIPLE_C_SIGNTOOL verify /pa $File.FullName 2>&1 | ForEach-Object { "$_" }
|
||||
$verifyOutput = & $env:TRIPLE_C_SIGNTOOL verify /pa $file.FullName 2>&1 | ForEach-Object { "$_" }
|
||||
$signtoolOk = ($LASTEXITCODE -eq 0)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not $signtoolOk) { $output | Write-Host }
|
||||
if (-not $signtoolOk) { $verifyOutput | Write-Host }
|
||||
|
||||
# And the timestamp, which signtool verify does not require.
|
||||
$sig = Get-AuthenticodeSignature -FilePath $File.FullName
|
||||
$sig = Get-AuthenticodeSignature -FilePath $file.FullName
|
||||
$timestamped = $null -ne $sig.TimeStamperCertificate
|
||||
|
||||
if ($signtoolOk -and $sig.Status -eq 'Valid' -and $timestamped) {
|
||||
Write-Host "OK $Label - $($sig.SignerCertificate.Subject)"
|
||||
return $true
|
||||
}
|
||||
Write-Host "FAIL $Label - status $($sig.Status), signtool $(if ($signtoolOk) {'ok'} else {'failed'}), timestamped $timestamped"
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-SignedLog {
|
||||
if ($env:TRIPLE_C_SIGN_LOG -and (Test-Path $env:TRIPLE_C_SIGN_LOG)) { return @(Get-Content $env:TRIPLE_C_SIGN_LOG) }
|
||||
return @()
|
||||
}
|
||||
|
||||
$failed = @()
|
||||
$nsisBuilt = $false
|
||||
foreach ($file in $files) {
|
||||
if (-not (Test-Signature $file $file.Name)) { $failed += $file.Name }
|
||||
if ($file.FullName -match '\\bundle\\nsis\\') { $nsisBuilt = $true }
|
||||
|
||||
if ($file.Extension -eq '.msi') {
|
||||
$extract = Join-Path ([IO.Path]::GetTempPath()) ('msi-verify-' + [guid]::NewGuid().ToString('N'))
|
||||
$proc = Start-Process msiexec.exe -Wait -PassThru `
|
||||
-ArgumentList '/a', "`"$($file.FullName)`"", '/qn', "TARGETDIR=`"$extract`""
|
||||
$inner = @()
|
||||
if ($proc.ExitCode -eq 0) { $inner = @(Get-ChildItem -Path $extract -Recurse -File -Include *.exe, *.dll) }
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Write-Host "FAIL $($file.Name) - administrative extract exited $($proc.ExitCode)"
|
||||
$failed += "$($file.Name) (extract)"
|
||||
} elseif ($inner.Count -eq 0) {
|
||||
Write-Host "FAIL $($file.Name) - contains no executable to check"
|
||||
$failed += "$($file.Name) (no executable)"
|
||||
}
|
||||
foreach ($f in $inner) {
|
||||
if (-not (Test-Signature $f "$($file.Name) > $($f.Name)")) { $failed += "$($file.Name) > $($f.Name)" }
|
||||
}
|
||||
Remove-Item -Recurse -Force $extract -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# What the NSIS installer carries but cannot be unpacked here. windows-sign.ps1
|
||||
# logs every file it signs. The app binary is signed in place under
|
||||
# target\release; the uninstaller is the file makensis wrote under the job's
|
||||
# temp directory (see windows-signing-setup.ps1) - and makensis ignores the
|
||||
# sign command's exit code for it, so without this a failure there is silent.
|
||||
if ($nsisBuilt) {
|
||||
$log = Get-SignedLog
|
||||
$appSigned = @($log | Where-Object { $_ -match '\\target\\release\\[^\\]+\.exe$' })
|
||||
if ($appSigned.Count -eq 0) {
|
||||
Write-Host 'FAIL app binary - no signature was logged for it before packaging'
|
||||
$failed += 'app binary'
|
||||
Write-Host "OK $($file.Name) - $($sig.SignerCertificate.Subject)"
|
||||
} else {
|
||||
Write-Host "OK app binary - signed before packaging ($($appSigned.Count)x)"
|
||||
Write-Host "FAIL $($file.Name) - status $($sig.Status), signtool $(if ($signtoolOk) {'ok'} else {'failed'}), timestamped $timestamped"
|
||||
$failed += $file.Name
|
||||
}
|
||||
}
|
||||
|
||||
# The NSIS uninstaller is signed from inside makensis, which ignores the sign
|
||||
# command's exit code, and it ends up embedded in the installer where the
|
||||
# checks above cannot reach it. windows-sign.ps1 logs every file it signs; the
|
||||
# uninstaller is the one makensis wrote under the job's temp directory (see
|
||||
# windows-signing-setup.ps1), so require at least one logged path there.
|
||||
$nsisBuilt = @($files | Where-Object { $_.FullName -match '\\bundle\\nsis\\' }).Count -gt 0
|
||||
if ($nsisBuilt) {
|
||||
$tmp = $env:TRIPLE_C_SIGN_TMP
|
||||
$uninstaller = @()
|
||||
if ($tmp) { $uninstaller = @($log | Where-Object { $_.StartsWith($tmp, [StringComparison]::OrdinalIgnoreCase) }) }
|
||||
if ($uninstaller.Count -eq 0) {
|
||||
Write-Host 'FAIL NSIS uninstaller - no signature was logged for it'
|
||||
$log = $env:TRIPLE_C_SIGN_LOG
|
||||
$signedInTmp = @()
|
||||
if ($tmp -and $log -and (Test-Path $log)) {
|
||||
$signedInTmp = @(Get-Content $log | Where-Object { $_.StartsWith($tmp, [StringComparison]::OrdinalIgnoreCase) })
|
||||
}
|
||||
if ($signedInTmp.Count -eq 0) {
|
||||
Write-Host 'FAIL NSIS uninstaller - no successful signature was logged for it'
|
||||
$failed += 'NSIS uninstaller'
|
||||
} else {
|
||||
Write-Host "OK NSIS uninstaller - signed as $($uninstaller[-1])"
|
||||
Write-Host "OK NSIS uninstaller - signed as $($signedInTmp[-1])"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) { throw "Not validly signed: $($failed -join ', ')" }
|
||||
Write-Host 'Everything that ships is signed and timestamped.'
|
||||
Write-Host "All $(@($files).Count) files are signed and timestamped."
|
||||
|
||||
Reference in New Issue
Block a user