Keep copied API requests private, bounded and one-shot

Save every existing reference response privately, including errors and partial POST results. Preserve private header files, reject unsafe preparation and URLs, and stop on redirects or transport errors without retries. Exercise all eight copied GET/POST examples against real synthetic HTTP/TLS peers, including the old response exposure.

Contract-Neutral: Harden existing API documentation recipes and their shipped mirror with connected tests; no runtime, endpoint, authentication, scope, action, dependency or permission contract changes.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-05 01:52:58 +01:00
parent e2241074f8
commit aece6b5217
3 changed files with 677 additions and 66 deletions
+91 -33
View File
@@ -17,7 +17,8 @@ secret does not appear in shell history or process arguments.
On a trusted machine, as the account running curl, prepare the file and open
it in an editor. This preserves an existing file, restricts its directory and
stops before editing if preparation fails or a credential path is a symlink:
stops before editing if preparation fails or a credential path is symlinked or
not a regular file:
```bash
(
@@ -29,6 +30,10 @@ stops before editing if preparation fails or a credential path is a symlink:
printf 'Refusing a symlinked credential path.\n' >&2
exit 1
fi
if [ -e "$auth_file" ] && [ ! -f "$auth_file" ]; then
printf 'Credential file must be a regular file.\n' >&2
exit 1
fi
mkdir -p "$auth_dir"
chmod 700 "$auth_dir"
touch "$auth_file"
@@ -43,20 +48,82 @@ In the editor, save just this line, replacing `<token>` with the API token:
X-API-Token: <token>
```
Then make a read-only request (curl 7.76 or later):
Define this helper in the same Bash session (curl 7.76 or later). The examples
on this page use it to make **one request**, print only the HTTP status and save
each response to a new owner-only file. This keeps resource names, audit details
and credential-bearing error bodies out of terminal output and recordings.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/state/summary
pulse_api() (
set -eu
umask 077
if [ "$#" -ne 2 ]; then
printf 'Usage: pulse_api GET|POST /api/path\n' >&2
exit 2
fi
method=$1
api_path=$2
case "$api_path" in
/api/*) ;;
*) printf 'Use an API path on this Pulse instance, not a full URL.\n' >&2; exit 2 ;;
esac
case "$method" in
GET) set -- ;;
POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;
*) printf 'This example helper accepts only GET and POST.\n' >&2; exit 2 ;;
esac
auth_dir="$HOME/.config/pulse"
auth_file="$auth_dir/api-header"
if [ -L "$HOME/.config" ] || [ -L "$auth_dir" ] || [ -L "$auth_file" ] || [ ! -f "$auth_file" ]; then
printf 'Prepare a regular, private header file first.\n' >&2
exit 1
fi
chmod 700 "$auth_dir" || exit "$?"
chmod 600 "$auth_file" || exit "$?"
result_file=$(mktemp "$auth_dir/api-response.XXXXXX") || exit "$?"
curl_exit=0
status=$(curl --disable --fail-with-body --silent --show-error \
--proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \
--header "@$auth_file" --output "$result_file" --write-out '%{http_code}' \
"$@" "http://127.0.0.1:7655$api_path") || curl_exit=$?
printf 'HTTP %s\nPrivate response: %s\n' "$status" "$result_file"
[ "$curl_exit" -eq 0 ] || exit "$curl_exit"
case "$status" in
2??) ;;
*) exit 1 ;;
esac
)
```
This loopback URL applies only when curl runs on the Pulse host. For remote
access, use your Pulse HTTPS URL and keep certificate verification enabled;
do not use `--insecure`. On an authentication-enabled instance, the protected
summary checks token access; `/api/health` is public and does not verify authentication.
`--fail-with-body` returns a non-zero exit on HTTP errors, including 401 and
403, while retaining the error response. Keep `--disable` first: it ignores
local curl configuration that could otherwise enable credential-bearing trace output.
Then make a read-only request with a `monitoring:read` token:
```bash
pulse_api GET /api/state/summary
```
The default loopback origin applies only when curl runs on the Pulse host and
explicitly bypasses proxy environment settings. For remote access, replace
`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace
`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate
verification enabled; for a private CA, add `--cacert` with a CA file you
verified separately, never `--insecure`. Do not put credentials in the URL.
Open the reported response file privately to inspect it, including on failure;
share only the relevant redacted error, not the whole response or file. Each
call preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit
and retain the error body; redirects are not followed and are not success.
The connection limit is five seconds and the whole request limit is twenty
seconds. A partial file after a transport error is not a complete result.
No request is retried automatically, especially a POST whose outcome is
uncertain: inspect the existing action and target before deciding what to do.
A 2xx response is only HTTP success, not proof that an action completed.
Keep `--disable` first: it ignores local curl configuration that could enable
trace output, bypass TLS or follow redirects. On an authentication-enabled
instance, the protected summary checks token access; `/api/health` is public
and does not verify authentication. The helper is local example code, not an
installed Pulse command; define it again in a new shell before using these
examples. Dispose of response files under your normal private-data policy.
Do not paste tokens into command lines, URLs or issue reports. Keep the header
file private and outside shared repositories and diagnostics; do not use curl
@@ -67,7 +134,7 @@ needed in Pulse's API Access settings.
### Bearer Token
The same API token can use a Bearer header instead. Replace the header file's
line in the editor with the following, then use the same curl command above;
line in the editor with the following, then use the same helper above;
do not send both authentication headers:
```text
@@ -326,12 +393,11 @@ Returns the canonical fleet connections ledger with per-row fleet-governance sta
The payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.
Use the private header file prepared in [Authentication](#-authentication),
Define the private-response helper in [Authentication](#-authentication),
with `settings:read` on the token:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/connections
pulse_api GET /api/connections
```
### Unified Action Planning
@@ -339,9 +405,10 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
The existing action API separates planning, approval and execution. Run these
steps separately, inspecting each response before moving on; do not paste the
whole sequence as an unattended recovery script. Use the private header file
from [Authentication](#-authentication), not a token in a command or environment
assignment. For remote access, replace the loopback origin with your Pulse HTTPS
URL and keep certificate verification enabled.
and `pulse_api` helper from [Authentication](#-authentication), not a token in a
command or environment assignment. Inspect each reported private response file
before proceeding. For remote access, change the helper as described there,
using your Pulse HTTPS origin with certificate verification enabled.
| Endpoint | Token scope and access |
| --- | --- |
@@ -365,8 +432,7 @@ not an action-control or audit token.
An empty capabilities list means there is nothing to plan for that resource.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/agent/resource-capabilities/vm%3A42
pulse_api GET /api/agent/resource-capabilities/vm%3A42
```
2. **Plan only.** Use an advertised capability and its actual parameter schema;
@@ -375,9 +441,7 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
authenticated credential, not a caller-supplied `requestedBy` value.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
--header 'Content-Type: application/json' --request POST --data-binary @- \
http://127.0.0.1:7655/api/actions/plan <<'JSON'
pulse_api POST /api/actions/plan <<'JSON'
{
"requestId": "manual-recovery-123",
"resourceId": "vm:42",
@@ -398,9 +462,7 @@ or currently available.
response rather than weakening the policy or creating another recovery action.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
--header 'Content-Type: application/json' --request POST --data-binary @- \
http://127.0.0.1:7655/api/actions/act_.../decision <<'JSON'
pulse_api POST /api/actions/act_.../decision <<'JSON'
{
"outcome": "approved",
"reason": "Inside maintenance window",
@@ -413,9 +475,7 @@ JSON
approval-free executable plan, use the executor's authorised header file:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
--header 'Content-Type: application/json' --request POST --data-binary @- \
http://127.0.0.1:7655/api/actions/act_.../execute <<'JSON'
pulse_api POST /api/actions/act_.../execute <<'JSON'
{
"reason": "Execute approved recovery",
"planHash": "sha256:..."
@@ -431,13 +491,11 @@ resource's observed state.
5. **Read the audit and events**, using a private header file with audit access:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
'http://127.0.0.1:7655/api/audit/actions?resourceId=vm%3A42&limit=10'
pulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'
```
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/audit/actions/act_.../events
pulse_api GET /api/audit/actions/act_.../events
```
When audit logging is unavailable, use Pulse's action detail in the signed-in
+91 -33
View File
@@ -17,7 +17,8 @@ secret does not appear in shell history or process arguments.
On a trusted machine, as the account running curl, prepare the file and open
it in an editor. This preserves an existing file, restricts its directory and
stops before editing if preparation fails or a credential path is a symlink:
stops before editing if preparation fails or a credential path is symlinked or
not a regular file:
```bash
(
@@ -29,6 +30,10 @@ stops before editing if preparation fails or a credential path is a symlink:
printf 'Refusing a symlinked credential path.\n' >&2
exit 1
fi
if [ -e "$auth_file" ] && [ ! -f "$auth_file" ]; then
printf 'Credential file must be a regular file.\n' >&2
exit 1
fi
mkdir -p "$auth_dir"
chmod 700 "$auth_dir"
touch "$auth_file"
@@ -43,20 +48,82 @@ In the editor, save just this line, replacing `<token>` with the API token:
X-API-Token: <token>
```
Then make a read-only request (curl 7.76 or later):
Define this helper in the same Bash session (curl 7.76 or later). The examples
on this page use it to make **one request**, print only the HTTP status and save
each response to a new owner-only file. This keeps resource names, audit details
and credential-bearing error bodies out of terminal output and recordings.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/state/summary
pulse_api() (
set -eu
umask 077
if [ "$#" -ne 2 ]; then
printf 'Usage: pulse_api GET|POST /api/path\n' >&2
exit 2
fi
method=$1
api_path=$2
case "$api_path" in
/api/*) ;;
*) printf 'Use an API path on this Pulse instance, not a full URL.\n' >&2; exit 2 ;;
esac
case "$method" in
GET) set -- ;;
POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;
*) printf 'This example helper accepts only GET and POST.\n' >&2; exit 2 ;;
esac
auth_dir="$HOME/.config/pulse"
auth_file="$auth_dir/api-header"
if [ -L "$HOME/.config" ] || [ -L "$auth_dir" ] || [ -L "$auth_file" ] || [ ! -f "$auth_file" ]; then
printf 'Prepare a regular, private header file first.\n' >&2
exit 1
fi
chmod 700 "$auth_dir" || exit "$?"
chmod 600 "$auth_file" || exit "$?"
result_file=$(mktemp "$auth_dir/api-response.XXXXXX") || exit "$?"
curl_exit=0
status=$(curl --disable --fail-with-body --silent --show-error \
--proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \
--header "@$auth_file" --output "$result_file" --write-out '%{http_code}' \
"$@" "http://127.0.0.1:7655$api_path") || curl_exit=$?
printf 'HTTP %s\nPrivate response: %s\n' "$status" "$result_file"
[ "$curl_exit" -eq 0 ] || exit "$curl_exit"
case "$status" in
2??) ;;
*) exit 1 ;;
esac
)
```
This loopback URL applies only when curl runs on the Pulse host. For remote
access, use your Pulse HTTPS URL and keep certificate verification enabled;
do not use `--insecure`. On an authentication-enabled instance, the protected
summary checks token access; `/api/health` is public and does not verify authentication.
`--fail-with-body` returns a non-zero exit on HTTP errors, including 401 and
403, while retaining the error response. Keep `--disable` first: it ignores
local curl configuration that could otherwise enable credential-bearing trace output.
Then make a read-only request with a `monitoring:read` token:
```bash
pulse_api GET /api/state/summary
```
The default loopback origin applies only when curl runs on the Pulse host and
explicitly bypasses proxy environment settings. For remote access, replace
`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace
`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate
verification enabled; for a private CA, add `--cacert` with a CA file you
verified separately, never `--insecure`. Do not put credentials in the URL.
Open the reported response file privately to inspect it, including on failure;
share only the relevant redacted error, not the whole response or file. Each
call preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit
and retain the error body; redirects are not followed and are not success.
The connection limit is five seconds and the whole request limit is twenty
seconds. A partial file after a transport error is not a complete result.
No request is retried automatically, especially a POST whose outcome is
uncertain: inspect the existing action and target before deciding what to do.
A 2xx response is only HTTP success, not proof that an action completed.
Keep `--disable` first: it ignores local curl configuration that could enable
trace output, bypass TLS or follow redirects. On an authentication-enabled
instance, the protected summary checks token access; `/api/health` is public
and does not verify authentication. The helper is local example code, not an
installed Pulse command; define it again in a new shell before using these
examples. Dispose of response files under your normal private-data policy.
Do not paste tokens into command lines, URLs or issue reports. Keep the header
file private and outside shared repositories and diagnostics; do not use curl
@@ -67,7 +134,7 @@ needed in Pulse's API Access settings.
### Bearer Token
The same API token can use a Bearer header instead. Replace the header file's
line in the editor with the following, then use the same curl command above;
line in the editor with the following, then use the same helper above;
do not send both authentication headers:
```text
@@ -326,12 +393,11 @@ Returns the canonical fleet connections ledger with per-row fleet-governance sta
The payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.
Use the private header file prepared in [Authentication](#-authentication),
Define the private-response helper in [Authentication](#-authentication),
with `settings:read` on the token:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/connections
pulse_api GET /api/connections
```
### Unified Action Planning
@@ -339,9 +405,10 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
The existing action API separates planning, approval and execution. Run these
steps separately, inspecting each response before moving on; do not paste the
whole sequence as an unattended recovery script. Use the private header file
from [Authentication](#-authentication), not a token in a command or environment
assignment. For remote access, replace the loopback origin with your Pulse HTTPS
URL and keep certificate verification enabled.
and `pulse_api` helper from [Authentication](#-authentication), not a token in a
command or environment assignment. Inspect each reported private response file
before proceeding. For remote access, change the helper as described there,
using your Pulse HTTPS origin with certificate verification enabled.
| Endpoint | Token scope and access |
| --- | --- |
@@ -365,8 +432,7 @@ not an action-control or audit token.
An empty capabilities list means there is nothing to plan for that resource.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/agent/resource-capabilities/vm%3A42
pulse_api GET /api/agent/resource-capabilities/vm%3A42
```
2. **Plan only.** Use an advertised capability and its actual parameter schema;
@@ -375,9 +441,7 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
authenticated credential, not a caller-supplied `requestedBy` value.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
--header 'Content-Type: application/json' --request POST --data-binary @- \
http://127.0.0.1:7655/api/actions/plan <<'JSON'
pulse_api POST /api/actions/plan <<'JSON'
{
"requestId": "manual-recovery-123",
"resourceId": "vm:42",
@@ -398,9 +462,7 @@ or currently available.
response rather than weakening the policy or creating another recovery action.
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
--header 'Content-Type: application/json' --request POST --data-binary @- \
http://127.0.0.1:7655/api/actions/act_.../decision <<'JSON'
pulse_api POST /api/actions/act_.../decision <<'JSON'
{
"outcome": "approved",
"reason": "Inside maintenance window",
@@ -413,9 +475,7 @@ JSON
approval-free executable plan, use the executor's authorised header file:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
--header 'Content-Type: application/json' --request POST --data-binary @- \
http://127.0.0.1:7655/api/actions/act_.../execute <<'JSON'
pulse_api POST /api/actions/act_.../execute <<'JSON'
{
"reason": "Execute approved recovery",
"planHash": "sha256:..."
@@ -431,13 +491,11 @@ resource's observed state.
5. **Read the audit and events**, using a private header file with audit access:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
'http://127.0.0.1:7655/api/audit/actions?resourceId=vm%3A42&limit=10'
pulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'
```
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
http://127.0.0.1:7655/api/audit/actions/act_.../events
pulse_api GET /api/audit/actions/act_.../events
```
When audit logging is unavailable, use Pulse's action detail in the signed-in
+495
View File
@@ -0,0 +1,495 @@
package installtests
import (
"context"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
)
// Execute the actual reference recipes against synthetic peers only. In
// particular, action examples must not acquire an automatic retry or emit a
// private response just because a POST failed or its response was incomplete.
func pulseAPIRecipes(doc string) (preparation, helper string, calls []string, err error) {
for _, match := range regexp.MustCompile("(?s)```bash\n(.*?)```").FindAllStringSubmatch(doc, -1) {
block := match[1]
switch {
case strings.Contains(block, "api-header") && strings.Contains(block, "vi "):
preparation = block
case strings.HasPrefix(block, "pulse_api() ("):
helper = block
case strings.HasPrefix(block, "pulse_api "):
calls = append(calls, block)
case strings.Contains(block, "curl "):
return "", "", nil, fmt.Errorf("request bypasses the private-response helper")
}
}
if preparation == "" || helper == "" || len(calls) != 8 {
err = fmt.Errorf("missing credential/helper recipe or request examples (got %d)", len(calls))
}
return
}
func pulseAPIReference(t *testing.T) (string, string, []string) {
t.Helper()
doc, err := os.ReadFile(repoFile("docs", "API.md"))
if err != nil {
t.Fatal(err)
}
mirror, err := os.ReadFile(repoFile("frontend-modern", "public", "docs", "API.md"))
if err != nil || string(doc) != string(mirror) {
t.Fatal("shipped API reference differs from the tested recipes")
}
prep, helper, calls, err := pulseAPIRecipes(string(doc))
if err != nil {
t.Fatal(err)
}
return prep, helper, calls
}
func runPulseAPIRecipe(t *testing.T, script, home, bin string, extraEnv ...string) (string, int) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "bash", "-c", script)
cmd.Env = append(os.Environ(), "HOME="+home, "PATH="+bin+":"+os.Getenv("PATH"),
"http_proxy=", "HTTP_PROXY=", "https_proxy=", "HTTPS_PROXY=", "ALL_PROXY=", "all_proxy=", "NO_PROXY=", "no_proxy=")
cmd.Env = append(cmd.Env, extraEnv...)
out, err := cmd.CombinedOutput()
if ctx.Err() != nil {
t.Fatal("API recipe exceeded the test watchdog; its own deadline did not complete")
}
if err == nil {
return string(out), 0
}
if failure, ok := err.(*exec.ExitError); ok {
return string(out), failure.ExitCode()
}
t.Fatal(err)
return "", -1
}
func TestPulseAPIGuidancePreparationIsPrivateAndFailClosed(t *testing.T) {
prep, _, _ := pulseAPIReference(t)
for _, mode := range []string{"new", "existing", "config symlink", "directory symlink", "file symlink", "dangling symlink", "non-regular", "mkdir failure", "chmod failure", "touch failure", "editor failure"} {
t.Run(mode, func(t *testing.T) {
home, bin := t.TempDir(), t.TempDir()
dir := filepath.Join(home, ".config", "pulse")
file := filepath.Join(dir, "api-header")
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
if mode == "new" || mode == "mkdir failure" {
if err := os.Remove(dir); err != nil {
t.Fatal(err)
}
}
if mode == "existing" {
if err := os.WriteFile(file, []byte("synthetic-existing-header"), 0644); err != nil {
t.Fatal(err)
}
}
if mode == "non-regular" {
if err := os.Mkdir(file, 0700); err != nil {
t.Fatal(err)
}
}
target := filepath.Join(home, "unchanged")
if strings.Contains(mode, "symlink") {
link := file
if mode == "config symlink" || mode == "directory symlink" {
link = dir
if mode == "config symlink" {
link = filepath.Join(home, ".config")
}
if err := os.Remove(dir); err != nil {
t.Fatal(err)
}
if mode == "config symlink" {
if err := os.Remove(link); err != nil {
t.Fatal(err)
}
}
if err := os.Mkdir(target, 0755); err != nil {
t.Fatal(err)
}
} else if mode == "file symlink" {
if err := os.WriteFile(target, []byte("synthetic-existing-header"), 0644); err != nil {
t.Fatal(err)
}
}
if err := os.Symlink(target, link); err != nil {
t.Fatal(err)
}
}
edited := filepath.Join(home, "editor-started")
editor := "#!/bin/sh\nprintf started > \"$API_EDITED\"\n"
if mode == "editor failure" {
editor += "exit 7\n"
}
if err := os.WriteFile(filepath.Join(bin, "vi"), []byte(editor), 0700); err != nil {
t.Fatal(err)
}
if strings.HasSuffix(mode, " failure") && mode != "editor failure" {
name := strings.TrimSuffix(mode, " failure")
if err := os.WriteFile(filepath.Join(bin, name), []byte("#!/bin/sh\nexit 7\n"), 0700); err != nil {
t.Fatal(err)
}
}
output, exit := runPulseAPIRecipe(t, prep, home, bin, "API_EDITED="+edited)
_, editorErr := os.Stat(edited)
if mode == "new" || mode == "existing" {
if exit != 0 || editorErr != nil {
t.Fatalf("safe preparation failed: exit %d", exit)
}
for path, perm := range map[string]os.FileMode{dir: 0700, file: 0600} {
info, err := os.Stat(path)
if err != nil || info.Mode().Perm() != perm {
t.Fatal("credential paths are not private")
}
}
if mode == "existing" {
data, err := os.ReadFile(file)
if err != nil || string(data) != "synthetic-existing-header" {
t.Fatal("preparation did not preserve the existing header")
}
}
} else if exit == 0 || (mode != "editor failure" && !os.IsNotExist(editorErr)) {
t.Fatal("unsafe or failed preparation reached the editor or succeeded")
}
if mode == "file symlink" {
data, err := os.ReadFile(target)
info, statErr := os.Stat(target)
if err != nil || statErr != nil || string(data) != "synthetic-existing-header" || info.Mode().Perm() != 0644 {
t.Fatal("preparation modified the symlink target")
}
}
if mode == "config symlink" || mode == "directory symlink" {
info, err := os.Stat(target)
if err != nil || info.Mode().Perm() != 0755 {
t.Fatal("preparation changed the target directory")
}
}
if strings.Contains(output, "synthetic-existing-header") {
t.Fatal("preparation disclosed a header")
}
})
}
}
func TestPulseAPIGuidanceHelperStopsBeforeCurlOnUnsafeFilesOrPreparation(t *testing.T) {
_, helper, _ := pulseAPIReference(t)
for _, mode := range []string{"missing", "file symlink", "config symlink", "directory symlink", "non-regular", "chmod failure", "mktemp failure", "unsupported method", "full URL", "extra argument"} {
t.Run(mode, func(t *testing.T) {
home, bin := t.TempDir(), t.TempDir()
dir := filepath.Join(home, ".config", "pulse")
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
file := filepath.Join(dir, "api-header")
if mode != "missing" && mode != "file symlink" && mode != "non-regular" {
if err := os.WriteFile(file, []byte("synthetic-header"), 0600); err != nil {
t.Fatal(err)
}
}
if mode == "file symlink" {
if err := os.Symlink(filepath.Join(home, "absent"), file); err != nil {
t.Fatal(err)
}
}
if mode == "config symlink" || mode == "directory symlink" {
link := dir
if mode == "config symlink" {
link = filepath.Join(home, ".config")
}
if err := os.Rename(link, link+"-retained"); err != nil {
t.Fatal(err)
}
if err := os.Symlink(link+"-retained", link); err != nil {
t.Fatal(err)
}
}
if mode == "non-regular" {
if err := os.Mkdir(file, 0700); err != nil {
t.Fatal(err)
}
}
called := filepath.Join(home, "curl-called")
if err := os.WriteFile(filepath.Join(bin, "curl"), []byte("#!/bin/sh\nprintf started > \"$API_CALLED\"\n"), 0700); err != nil {
t.Fatal(err)
}
if strings.HasSuffix(mode, " failure") {
if err := os.WriteFile(filepath.Join(bin, strings.TrimSuffix(mode, " failure")), []byte("#!/bin/sh\nexit 7\n"), 0700); err != nil {
t.Fatal(err)
}
}
call := "pulse_api GET /api/state/summary"
switch mode {
case "unsupported method":
call = "pulse_api DELETE /api/actions/act_example"
case "full URL":
call = "pulse_api GET https://not-this-instance.invalid/api/state"
case "extra argument":
call += " --location"
}
// Bash disables implicit errexit inside functions used as conditions.
// The documented helper must still stop on chmod/mktemp failure.
script := helper + "\nif " + call + "; then exit 0; else exit $?; fi\n"
_, exit := runPulseAPIRecipe(t, script, home, bin, "API_CALLED="+called)
_, err := os.Stat(called)
if exit == 0 || !os.IsNotExist(err) {
t.Fatal("invalid input or failed preparation reached curl")
}
})
}
}
type apiRecipeRequest struct{ method, uri, token, contentType, body string }
func TestPulseAPIGuidanceCopiedRequestsKeepResponsesPrivateAndOneShot(t *testing.T) {
_, helper, calls := pulseAPIReference(t)
paths := []string{"/api/state/summary", "/api/connections", "/api/agent/resource-capabilities/vm%3A42", "/api/actions/plan", "/api/actions/act_.../decision", "/api/actions/act_.../execute", "/api/audit/actions?resourceId=vm%3A42&limit=10", "/api/audit/actions/act_.../events"}
for index, call := range calls {
for _, scenario := range []string{"success", "401", "403", "redirect", "partial", "untrusted HTTPS", "trusted HTTPS"} {
t.Run(fmt.Sprintf("request-%d/%s", index, scenario), func(t *testing.T) {
home, bin := t.TempDir(), t.TempDir()
dir := filepath.Join(home, ".config", "pulse")
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
const token = "synthetic-fixture-secret"
const response = `{"token":"synthetic-fixture-secret","resource":"private-fixture-name","audit":"private-fixture-event"}`
if err := os.WriteFile(filepath.Join(dir, "api-header"), []byte("X-API-Token: "+token+"\n"), 0644); err != nil {
t.Fatal(err)
}
retained := filepath.Join(dir, "api-response.retained")
if err := os.WriteFile(retained, []byte("previous-result"), 0600); err != nil {
t.Fatal(err)
}
capture, trace := filepath.Join(home, "argv"), filepath.Join(home, "trace")
realCurl, err := exec.LookPath("curl")
if err != nil {
t.Fatal("curl is required to verify copied API requests")
}
wrapper := "#!/bin/sh\nprintf '%s\\0' \"$@\" > \"$API_ARGV\"\nexec \"$API_CURL\" \"$@\"\n"
if err := os.WriteFile(filepath.Join(bin, "curl"), []byte(wrapper), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(home, ".curlrc"), []byte("insecure\nlocation\ntrace = "+trace+"\n"), 0600); err != nil {
t.Fatal(err)
}
requests := make(chan apiRecipeRequest, 4)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
requests <- apiRecipeRequest{r.Method, r.URL.RequestURI(), r.Header.Get("X-API-Token"), r.Header.Get("Content-Type"), string(body)}
switch scenario {
case "401":
w.WriteHeader(http.StatusUnauthorized)
case "403":
w.WriteHeader(http.StatusForbidden)
case "redirect":
w.Header().Set("Location", "/should-not-receive-header")
w.WriteHeader(http.StatusFound)
case "partial":
w.Header().Set("Content-Length", fmt.Sprint(len(response)+100))
}
_, _ = io.WriteString(w, response)
})
var server *httptest.Server
recipe := helper
if strings.Contains(scenario, "HTTPS") {
server = httptest.NewUnstartedServer(handler)
server.Config.ErrorLog = log.New(io.Discard, "", 0)
server.StartTLS()
recipe = strings.Replace(recipe, "--proto '=http' --noproxy 127.0.0.1", "--proto '=https'", 1)
if scenario == "trusted HTTPS" {
ca := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw})
if err := os.WriteFile(filepath.Join(home, "ca.pem"), ca, 0600); err != nil {
t.Fatal(err)
}
recipe = strings.Replace(recipe, "curl --disable ", `curl --disable --cacert "$HOME/ca.pem" `, 1)
}
} else {
server = httptest.NewServer(handler)
}
t.Cleanup(server.Close)
recipe = strings.Replace(recipe, "http://127.0.0.1:7655", server.URL, 1)
extra := []string{"API_ARGV=" + capture, "API_CURL=" + realCurl}
if !strings.Contains(scenario, "HTTPS") {
// The local recipe must not send credentials to an ambient proxy.
extra = append(extra, "http_proxy=http://127.0.0.1:1", "ALL_PROXY=http://127.0.0.1:1")
}
output, exit := runPulseAPIRecipe(t, recipe+"\n"+call, home, bin, extra...)
wantExit := map[string]int{"success": 0, "401": 22, "403": 22, "redirect": 1, "partial": 18, "untrusted HTTPS": 60, "trusted HTTPS": 0}[scenario]
if exit != wantExit {
t.Fatalf("request exit %d, want %d", exit, wantExit)
}
argv, err := os.ReadFile(capture)
if err != nil || !strings.HasPrefix(string(argv), "--disable\x00") {
t.Fatal("curl did not ignore configuration first")
}
for _, sensitive := range []string{token, "private-fixture-name", "private-fixture-event"} {
if strings.Contains(output, sensitive) || strings.Contains(string(argv), sensitive) {
t.Fatal("request printed private credentials or response data")
}
}
if _, err := os.Stat(trace); !os.IsNotExist(err) {
t.Fatal("local curl configuration enabled tracing")
}
responses, err := filepath.Glob(filepath.Join(dir, "api-response.??????"))
if err != nil || len(responses) != 1 || !strings.Contains(output, responses[0]) {
t.Fatal("response was not saved to one new, reported private file")
}
for path, perm := range map[string]os.FileMode{dir: 0700, filepath.Join(dir, "api-header"): 0600, responses[0]: 0600} {
info, err := os.Stat(path)
if err != nil || info.Mode().Perm() != perm {
t.Fatal("request paths are not private")
}
}
if data, err := os.ReadFile(retained); err != nil || string(data) != "previous-result" {
t.Fatal("request replaced an earlier response")
}
if scenario == "untrusted HTTPS" {
if len(requests) != 0 {
t.Fatal("untrusted TLS peer received credentials")
}
} else {
if len(requests) != 1 {
t.Fatal("request was missing, redirected or retried")
}
got := <-requests
method := "GET"
contentType, body := "", ""
if index >= 3 && index <= 5 {
method, contentType = "POST", "application/json"
_, body, _ = strings.Cut(call, "<<'JSON'\n")
body = strings.TrimSuffix(body, "JSON\n")
}
if got != (apiRecipeRequest{method, paths[index], token, contentType, body}) {
t.Fatal("request method, endpoint, private header or JSON body differs from the example")
}
if data, err := os.ReadFile(responses[0]); err != nil || string(data) != response {
t.Fatal("HTTP response, including failure or partial body, was not retained privately")
}
}
})
}
}
}
func TestPulseAPIGuidanceTimedOutPOSTIsNotRepeated(t *testing.T) {
_, helper, calls := pulseAPIReference(t)
home, bin := t.TempDir(), t.TempDir()
dir := filepath.Join(home, ".config", "pulse")
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "api-header"), []byte("X-API-Token: synthetic-timeout-secret\n"), 0600); err != nil {
t.Fatal(err)
}
requests := make(chan struct{}, 4)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.Copy(io.Discard, r.Body)
requests <- struct{}{}
w.Header().Set("Content-Length", "100")
_, _ = io.WriteString(w, "private-partial-response")
w.(http.Flusher).Flush()
<-r.Context().Done()
}))
defer server.Close()
if !strings.Contains(helper, "--connect-timeout 5 --max-time 20") {
t.Fatal("documented request deadlines are missing")
}
// Use the actual documented twenty-second limit, not a test-only shortened
// deadline. An execution endpoint can have acted even when this read fails.
helper = strings.Replace(helper, "http://127.0.0.1:7655", server.URL, 1)
start := time.Now()
output, exit := runPulseAPIRecipe(t, helper+"\n"+calls[5], home, bin)
if exit != 28 || time.Since(start) > 25*time.Second || len(requests) != 1 {
t.Fatal("POST did not stop at its request deadline or was retried")
}
if strings.Contains(output, "private-partial-response") || strings.Contains(output, "synthetic-timeout-secret") {
t.Fatal("timed-out POST disclosed a private body or credential")
}
responses, err := filepath.Glob(filepath.Join(dir, "api-response.??????"))
if err != nil || len(responses) != 1 {
t.Fatal("timed-out POST did not retain its response")
}
if data, err := os.ReadFile(responses[0]); err != nil || string(data) != "private-partial-response" {
t.Fatal("partial response was not retained for private reconciliation")
}
}
func TestPulseAPIGuidanceExactParentPrintsPrivateResponse(t *testing.T) {
inputs := os.Getenv("PULSE_PROOF_INPUTS")
if inputs == "" {
t.Skip("exact-parent input is provided by the candidate source proof")
}
old, err := os.ReadFile(filepath.Join(inputs, "api-parent.md"))
if os.IsNotExist(err) {
t.Skip("unrelated proof input does not supply the exact parent")
}
if err != nil {
t.Fatal(err)
}
var request string
for _, match := range regexp.MustCompile("(?s)```bash\n(.*?)```").FindAllStringSubmatch(string(old), -1) {
if strings.Contains(match[1], "/api/state/summary") && strings.Contains(match[1], "curl ") {
request = match[1]
break
}
}
if request == "" {
t.Fatal("exact old reference summary command is missing")
}
home, bin := t.TempDir(), t.TempDir()
dir := filepath.Join(home, ".config", "pulse")
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "api-header"), []byte("X-API-Token: synthetic-parent-secret\n"), 0600); err != nil {
t.Fatal(err)
}
const response = "synthetic-parent-secret private-parent-resource"
requests := make(chan struct{}, 2)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests <- struct{}{}
if r.Header.Get("X-API-Token") != "synthetic-parent-secret" {
w.WriteHeader(http.StatusUnauthorized)
}
_, _ = io.WriteString(w, response)
}))
defer server.Close()
request = strings.Replace(request, "http://127.0.0.1:7655", server.URL, 1)
output, exit := runPulseAPIRecipe(t, request, home, bin)
if exit != 0 || len(requests) != 1 || !strings.Contains(output, response) {
t.Fatal("exact parent did not reproduce the private-response console exposure")
}
}
func TestPulseAPIGuidanceRejectsExactOldReference(t *testing.T) {
inputs := os.Getenv("PULSE_PROOF_INPUTS")
if inputs == "" {
t.Skip("exact-parent input is provided by the candidate source proof")
}
old, err := os.ReadFile(filepath.Join(inputs, "api-parent.md"))
if os.IsNotExist(err) {
t.Skip("unrelated proof input does not supply the exact parent")
}
if err != nil {
t.Fatal(err)
}
if _, _, _, err := pulseAPIRecipes(string(old)); err == nil {
t.Fatal("old direct, unbounded console-response examples were accepted")
}
}