Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ entry, add a probe for it:
1. **A script here** — short, deterministic, side-effect-free, printing
the `PROBE:` line above. One failure mode per script: a probe that
conflates two restrictions makes the report ambiguous when it fails.
Hold a command's output in a variable, not a file: `/tmp` is read-only
inside the sandbox, and a failed redirection skips the command and
exits 1, which a probe reading exit codes takes for the command's own
answer. Probe 1 once passed every agent that way. Add a test under
[`../tests/`](../tests/) that runs the script against a fake binary.
2. **An interpretation table in `SKILL.md`** — three to five rows
mapping result strings to ✓ / ✗ / ⊘ / ⚠.
3. **A remediation link** to the catalogue section that fixes it. Link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ elif [ ! -S "$SSH_AUTH_SOCK" ]; then
echo "PROBE: ssh-agent → ✗ (socket file at SSH_AUTH_SOCK not stat-able from inside sandbox)"
echo " SSH_AUTH_SOCK=$SSH_AUTH_SOCK"
else
ssh-add -l > /tmp/ssh-add.out 2>&1; rc=$?
# Held in a variable, not a file: /tmp is read-only inside the sandbox, and a
# failed redirection never runs ssh-add but exits 1, the "no identities" code.
out=$(ssh-add -l 2>&1); rc=$?
case "$rc" in
0) echo "PROBE: ssh-agent → ✓ ($(wc -l < /tmp/ssh-add.out | tr -d ' ') identities listed)" ;;
0) echo "PROBE: ssh-agent → ✓ ($(printf '%s\n' "$out" | wc -l | tr -d ' ') identities listed)" ;;
1) echo "PROBE: ssh-agent → ✓ (agent reachable, no identities configured)" ;;
2) echo "PROBE: ssh-agent → ✗ (agent unreachable: $(head -1 /tmp/ssh-add.out))" ;;
*) echo "PROBE: ssh-agent → ⚠ (unexpected rc=$rc: $(head -1 /tmp/ssh-add.out))" ;;
2) echo "PROBE: ssh-agent → ✗ (agent unreachable: $(printf '%s\n' "$out" | head -1))" ;;
*) echo "PROBE: ssh-agent → ⚠ (unexpected rc=$rc: $(printf '%s\n' "$out" | head -1))" ;;
esac
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Probe 1 reports what `ssh-add -l` returned, and writes no file.

The probe used to capture `ssh-add -l` into `/tmp/ssh-add.out`. Inside
the sandbox `/tmp` is read-only, so the redirection failed, `ssh-add`
never ran, and the redirection's own exit code of 1 was read as "agent
reachable, no identities": a pass for an agent the probe never asked.
"""

from __future__ import annotations

import os
import socket
import stat
import subprocess
import tempfile
import time
import unittest
from pathlib import Path

PROBE = Path(__file__).resolve().parents[1] / "scripts" / "probe-1-ssh-agent.sh"
OLD_OUTPUT = Path("/tmp/ssh-add.out")


class ProbeOneSshAgent(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.tmp = Path(self._tmp.name)
self.bin = self.tmp / "bin"
self.bin.mkdir()
self.addCleanup(self._tmp.cleanup)
# The probe only checks that SSH_AUTH_SOCK is a socket; the fake
# ssh-add never connects to it. A sandbox may refuse bind(2), so fall
# back to the session's own agent socket.
self.sock_path = self.tmp / "agent.sock"
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
try:
sock.bind(str(self.sock_path))
except OSError:
existing = os.environ.get("SSH_AUTH_SOCK", "")
if not (existing and Path(existing).is_socket()):
self.skipTest("cannot bind a unix socket and no SSH_AUTH_SOCK to reuse")
self.sock_path = Path(existing)

def fake_ssh_add(self, output: str, rc: int) -> None:
script = self.bin / "ssh-add"
script.write_text(f"#!/bin/sh\nprintf '{output}'\nexit {rc}\n")
script.chmod(script.stat().st_mode | stat.S_IXUSR)

def run_probe(self) -> str:
env = {
"PATH": f"{self.bin}{os.pathsep}{os.environ.get('PATH', '')}",
"SSH_AUTH_SOCK": str(self.sock_path),
"TMPDIR": str(self.tmp),
}
result = subprocess.run(
["bash", str(PROBE)], env=env, capture_output=True, text=True, check=True
)
return result.stdout.strip()

def test_counts_listed_identities(self) -> None:
self.fake_ssh_add("4096 SHA256:aaa key-a (RSA)\n4096 SHA256:bbb key-b (RSA)\n", 0)
self.assertEqual(self.run_probe(), "PROBE: ssh-agent → ✓ (2 identities listed)")

def test_reports_unreachable_agent_with_its_message(self) -> None:
self.fake_ssh_add("Could not open a connection to your authentication agent.\n", 2)
self.assertEqual(
self.run_probe(),
"PROBE: ssh-agent → ✗ (agent unreachable: "
"Could not open a connection to your authentication agent.)",
)

def test_writes_nothing_under_tmp(self) -> None:
self.fake_ssh_add("4096 SHA256:aaa key-a (RSA)\n", 0)
started = time.time()
self.run_probe()
if OLD_OUTPUT.exists():
self.assertLess(OLD_OUTPUT.stat().st_mtime, started)


if __name__ == "__main__":
unittest.main()
Loading