Set up SSH script discovery
Run your own script on a host over SSH and record the system details and interfaces it prints.
An SSH credential logs in to a host, runs a script you write, and records the JSON object the script prints as the host's system details and interfaces.
The script runs as an ordinary SSH session, so it can do any work the account is allowed to do on each scan, such as clearing a cache or restarting a service, as well as reporting details. A script that reports nothing prints {}.
Beta
The SSH integration is in beta. Data collection may be incomplete and the credential fields may change in a future release. Please report anything that looks wrong.
Before you start
SSH credentials are created under Assets > Credentials and can be pointed at NetworkRemote hosts.
Creating a credential, assigning it, and overriding it on an individual host work the same way for every integration — see Creating a credential, Where a credential applies, and Auto-assignment. This guide covers only what is specific to SSH.
What gets discovered
Your script sets what Scanopy records. It prints one JSON object, and Scanopy applies each key in this table to the host or interface field it names:
| Key | Type | Fills | Description | Example |
|---|---|---|---|---|
hostname | string | hostname | The host's own hostname. | "nas-01" |
sys_name | string | sys_name | Administrative name, as SNMP sysName reports it. | "nas-01.example.lan" |
sys_descr | string | sys_descr | Free-text system description, typically OS and version. | "Debian GNU/Linux 12 (bookworm) 6.1.0-18-amd64" |
sys_object_id | string | sys_object_id | Vendor object identifier, as SNMP sysObjectID reports it. | "1.3.6.1.4.1.8072.3.2.10" |
sys_location | string | sys_location | Physical location. | "Rack 2, shelf 3" |
sys_contact | string | sys_contact | Contact person or team. | "[email protected]" |
chassis_id | string | chassis_id | Chassis identifier, usually the base MAC address. | "3c:ec:ef:12:34:56" |
manufacturer | string | manufacturer | Hardware manufacturer. | "Supermicro" |
model | string | model | Hardware model. | "X11SCL-F" |
serial_number | string | serial_number | Hardware serial number. | "ZM19AS012345" |
firmware_revision | string | firmware_revision | Firmware or BIOS version. | "2.1" |
software_revision | string | software_revision | Operating system or software version. | "12.5" |
management_url | string | management_url | URL of the host's management interface. | "https://nas-01.example.lan:5001" |
interfaces[].name | string | if_name | Interface name. Interfaces are matched to ones discovery already holds by name, then by MAC. | "eth0" |
interfaces[].descr | string | if_descr | Interface description, as SNMP ifDescr reports it. | "Intel Corporation I210 Gigabit" |
interfaces[].alias | string | if_alias | Operator-assigned interface label, as SNMP ifAlias reports it. | "uplink to core-sw-01" |
interfaces[].mac | string | mac_address | Interface MAC address. | "3c:ec:ef:12:34:57" |
interfaces[].speed_bps | integer | speed_bps | Link speed in bits per second. | 1000000000 |
interfaces[].admin_status | string (enum) | admin_status | Configured state: Up, Down or Testing. | "Up" |
interfaces[].oper_status | string (enum) | oper_status | Operational state: Up, Down, Testing, Unknown, Dormant, NotPresent or LowerLayerDown. | "Up" |
This script reads OS and hardware details on a Linux host from /etc/os-release and DMI:
#!/bin/sh
. /etc/os-release
dmi() { cat "/sys/class/dmi/id/$1" 2>/dev/null; }
printf '{"hostname":"%s","sys_descr":"%s %s","manufacturer":"%s","model":"%s","serial_number":"%s","firmware_revision":"%s","software_revision":"%s"}\n' \
"$(hostname -f)" "$PRETTY_NAME" "$(uname -r)" \
"$(dmi sys_vendor)" "$(dmi product_name)" "$(sudo -n cat /sys/class/dmi/id/product_serial)" \
"$(dmi bios_version)" "$VERSION_ID"On a Debian 12 server it prints:
{"hostname":"nas-01.example.lan","sys_descr":"Debian GNU/Linux 12 (bookworm) 6.1.0-18-amd64","manufacturer":"Supermicro","model":"X11SCL-F","serial_number":"ZM19AS012345","firmware_revision":"2.1","software_revision":"12"}To report interfaces, add an interfaces array:
{
"hostname": "nas-01.example.lan",
"interfaces": [
{ "name": "eth0", "mac": "3c:ec:ef:12:34:57", "speed_bps": 1000000000, "oper_status": "Up" },
{ "name": "eth1", "alias": "backup network", "oper_status": "Down" }
]
}Top-level keys describe the host. Interface keys sit in objects inside the top-level interfaces array. Scanopy matches each interface to one it already holds by name, then by MAC, and leaves interfaces the script does not print in place.
Scanopy reports keys outside the table under Not applied on the run and does not store them. A blank string counts as no value.
A value from the script outranks what SNMP, LLDP and the other discovery protocols report for the same field, and never replaces a value someone entered in Scanopy. Each value shows SSH script as its source.
Output rules
- Exactly one JSON object on standard output, at most 64 KiB. Write diagnostics to standard error, which the run shows when the script fails.
- Exit status 0. Any other status discards the output.
- Each key has the type the table gives it.
speed_bpsis a number, not a string, and the status values are case-sensitive (Up, notup). One wrong type rejects the whole document.
Prerequisites
-
A dedicated account on each host. The script runs with that account's permissions, so give it only what the script needs. The Linux example script needs root for one file:
/sys/class/dmi/id/product_serial. Grant that single command withvisudo -f /etc/sudoers.d/scanopy:scanopy ALL=(root) NOPASSWD: /usr/bin/cat /sys/class/dmi/id/product_serialWithout the rule,
sudo -nfails, the serial prints as a blank string, and Scanopy skips it. -
A private key or a password for that account. For a key, the public half goes in the account's
~/.ssh/authorized_keys. On Windows, a key for an account in the Administrators group goes inC:\ProgramData\ssh\administrators_authorized_keysinstead. For a password, the SSH server must allow password authentication. -
On Windows hosts, OpenSSH Server installed and running. It is an optional Windows feature.
-
The SSH port reachable over TCP from the daemon host. The default is 22.
Choosing a credential type
| Credential type | How it connects | Can be targeted at | Requires daemon |
|---|---|---|---|
| SSH KeyBeta | Connects over SSH with a private key. | NetworkRemote hosts | 0.17.19 or later |
| SSH PasswordBeta | Connects over SSH with a username and password. | NetworkRemote hosts | 0.17.19 or later |
Both types run the same script and apply the same output. SSH Key avoids storing a reusable password and works on servers that turn password login off.
Assign the credential to the hosts the script is written for. Assign it to a whole network only when the account, its key or password, and the script's commands exist on every host there that answers SSH. Each host that refuses the login reports Login refused on every run.
Writing and deploying the script
Deploy the script to each scanned host
Script takes one of three sources:
| Source | What Scanopy stores | Where the script lives |
|---|---|---|
| File on scanned host (default) | The path | On each scanned host |
| File on daemon host | The path | On the daemon host. The daemon reads it at each scan and sends its text |
| Enter value | The script text | In the Scanopy server database |
Use File on scanned host. Deploy the script to each host with your configuration management tool, such as Ansible, Puppet or Group Policy, and enter its path, for example /usr/local/bin/scanopy-inventory.sh or C:\Scanopy\scanopy-inventory.ps1. Scanopy stores only the path, and a script update goes out with the rest of the host's configuration. The path must be absolute for the Scanned Host OS, and a Windows path cannot contain ".
A File on daemon host path must be absolute for the credential's Daemon OS, and ~ is not expanded.
Scanned Host OS sets the shell the script runs in
Set Scanned Host OS to the OS of the hosts the credential is assigned to. The daemon sends one of these commands over SSH:
| Scanned Host OS | Script source | Command the daemon sends |
|---|---|---|
| Linux, macOS, BSD | File on scanned host | The path in single quotes, which the login shell runs as a program |
| Linux, macOS, BSD | File on daemon host, or Enter value | The script text, which the login shell runs |
| Windows | File on scanned host | powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "<path>" |
| Windows | File on daemon host, or Enter value | powershell -NoProfile -NonInteractive -EncodedCommand <text>, with the text base64-encoded as UTF-16LE |
The Windows commands run under either default shell of the Windows OpenSSH server, cmd.exe or PowerShell. A credential covers hosts of one OS. Create a second credential for hosts of the other.
Writing the script for Linux, macOS and BSD
A File on scanned host script runs as a program, so it needs a #!/bin/sh first line, like the example above, and the execute bit (chmod 755). Text from the other two sources runs in the account's login shell, the same as ssh scanopy@HOST 'script'. A #! first line is a comment to that shell, so write the text in the login shell's language.
printf '%s' inserts a value as-is, so a value containing " or \ produces invalid JSON. Build the object with jq -n --arg on hosts that have jq.
Writing the script for Windows
PowerShell runs the script with -NoProfile, so profile scripts add nothing to its output. This script reads the same details as the Linux example from CIM and the network adapter list, and prints them with ConvertTo-Json -Compress, which escapes quotes and backslashes in values:
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$bios = Get-CimInstance Win32_BIOS
$interfaces = @(Get-NetAdapter -Physical | ForEach-Object {
$nic = [ordered]@{
name = $_.Name
descr = $_.InterfaceDescription
mac = $_.MacAddress -replace '-', ':'
oper_status = if ($_.Status -eq 'Up') { 'Up' } else { 'Down' }
}
if ($_.Status -eq 'Up') { $nic.speed_bps = [int64]$_.Speed }
$nic
})
[ordered]@{
hostname = [System.Net.Dns]::GetHostName()
sys_descr = "$($os.Caption) $($os.Version)"
manufacturer = $cs.Manufacturer
model = $cs.Model
serial_number = $bios.SerialNumber
firmware_revision = $bios.SMBIOSBIOSVersion
software_revision = $os.Version
interfaces = $interfaces
} | ConvertTo-Json -Compress -Depth 3Get-NetAdapter writes MAC addresses with dashes, and the script converts them to colons. Speed is the link speed in bits per second, the number behind the LinkSpeed text such as 1 Gbps. The script reports it only for adapters that are up.
Testing the script
Run the script yourself as the same account before you save the credential. These commands send it the way the daemon does:
# File on scanned host, Linux, macOS, BSD
ssh scanopy@HOST /usr/local/bin/scanopy-inventory.sh | jq .
# File on daemon host or Enter value, Linux, macOS, BSD
ssh scanopy@HOST "$(cat scanopy-inventory.sh)" | jq .
# File on scanned host, Windows
ssh scanopy@HOST 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scanopy\scanopy-inventory.ps1"' | jq .Host key checking
Scanopy checks the host key on every run, before it sends any credentials:
- Host Key Fingerprint left blank. The first successful login pins the key the host presented, in the daemon's config file under
ssh_known_host_keys, keyed byip:port. A later run that sees a different key refuses the host and runs nothing. - Host Key Fingerprint set. Scanopy accepts only that key. Get it on the host with
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, or on Windows withssh-keygen -lf C:\ProgramData\ssh\ssh_host_ed25519_key.pub. A fingerprint suits a credential assigned to one host, because every host has its own key.
SSH Key
Create a key pair for Scanopy, without a passphrase or with one you enter in Key Passphrase:
ssh-keygen -t ed25519 -f scanopy_ed25519 -C scanopyAppend scanopy_ed25519.pub to the account's authorized keys file on each host, and paste the contents of scanopy_ed25519, including its BEGIN and END lines, into Private Key. To keep the key out of Scanopy, choose File on daemon host and enter the key file's path. The path must be absolute for the credential's Daemon OS, and ~ is not expanded. Scanopy reads OpenSSH keys, the format ssh-keygen writes by default, and PEM keys.
| Field | Required | Default | Description |
|---|---|---|---|
| Connection | |||
| SSH Port | Optional | 22 | |
| Authentication | |||
| Username | Required | None | Account the script runs as. Use a dedicated account with only the access the script needs. |
| Private KeySecret | Required | None | OpenSSH (ssh-keygen's default) or PEM private key whose public key is in the account's authorized_keys. |
| Key PassphraseSecret | Optional | None | Only if the private key is encrypted. |
| Script | |||
| Scanned Host OS | Required | Linux, macOS, BSD | Sets how the script runs, in the login shell on Linux, macOS and BSD or in PowerShell on Windows, and the path format for a file on the scanned host. One of: Linux, macOS, BSD, Windows. |
| Script | Required | None | Runs at each scan. Must print one JSON object of host fields. |
| Timeout (seconds) | Optional | 60 | The script is stopped and the run reports a failure after this long. |
| Host Key Fingerprint | Optional | None | Leave blank to trust the key each host presents the first time and refuse it if it later changes. Enter a fingerprint (ssh-keygen -lf) to require that key instead. |
SSH Password
Enter the account's password in Password, or choose File on daemon host and enter the path of a file on the daemon host that holds it. The path must be absolute for the credential's Daemon OS, and ~ is not expanded.
| Field | Required | Default | Description |
|---|---|---|---|
| Connection | |||
| SSH Port | Optional | 22 | |
| Authentication | |||
| Username | Required | None | Account the script runs as. Use a dedicated account with only the access the script needs. |
| PasswordSecret | Required | None | Password for that account. The server must allow password authentication. |
| Script | |||
| Scanned Host OS | Required | Linux, macOS, BSD | Sets how the script runs, in the login shell on Linux, macOS and BSD or in PowerShell on Windows, and the path format for a file on the scanned host. One of: Linux, macOS, BSD, Windows. |
| Script | Required | None | Runs at each scan. Must print one JSON object of host fields. |
| Timeout (seconds) | Optional | 60 | The script is stopped and the run reports a failure after this long. |
| Host Key Fingerprint | Optional | None | Leave blank to trust the key each host presents the first time and refuse it if it later changes. Enter a fingerprint (ssh-keygen -lf) to require that key instead. |
Verifying it works
- Run the network's discovery from Discover > Scans, or wait for its next scheduled run
- Open the finished run under Discover > Scans > Historical and select the Credentials tab
- Find the SSH credential's row. It lists each host the script ran on with its outcome. Applied means the script ran and Scanopy stored its output. Fields applied lists the keys Scanopy stored, and Not applied lists keys it did not.
- Check the row's warning count. Any warning appears on the run's Issues tab.
- Open the host. Its Details tab shows each value the script set with SSH script as the source, and its Interfaces tab shows the interfaces the script printed.
Troubleshooting
Each failed run appears in the SSH credential's row on the run's Credentials tab, with its outcome and the end of the script's standard error or the error message. It also raises a credential warning on the run's Issues tab. A failed run applies nothing, and the host keeps the values from its last successful run.
Login refused
The host rejected the username, key or password, and the script did not run. Test the same login from the daemon host:
ssh -i scanopy_ed25519 -o IdentitiesOnly=yes scanopy@HOST trueFor SSH Password, check that the server's sshd_config allows PasswordAuthentication. A per-host credential overrides the network default, so check which credential reached this host.
Host key changed
The host presented a different key from the pinned key, or from the credential's Host Key Fingerprint, and Scanopy did not log in. The run's detail names the key the host presented.
A reinstalled host or regenerated host keys produce this outcome. So does a different machine answering at that address. Confirm which it is before you trust the new key.
To trust the new key on a pinned host:
- Stop the daemon
- Open its config file (locations are listed under Configuration Methods) and delete the host's
ip:portentry fromssh_known_host_keys - Start the daemon
The next run pins the key the host presents. Stop the daemon before you edit the file, because a running daemon writes its config back and restores the entry.
For a credential with Host Key Fingerprint set, update the fingerprint instead.
Connection failed
The daemon reached the SSH port during the scan and then could not set up a session. The detail gives the reason. "could not read the private key" means the key is in a format Scanopy does not read, or it is encrypted and Key Passphrase is empty or wrong.
Timed out
The script did not finish within Timeout (seconds), 60 by default. Scanopy stops it and applies nothing. Raise the timeout, or check for a command waiting on input, such as sudo without -n asking for a password.
Exited with an error
The script exited with a status other than 0, and Scanopy discarded its output. The run shows the exit code and the end of standard error. A command that fails under set -e produces this outcome, and so does a missing command or a File on scanned host path that does not exist. A Linux, macOS or BSD script file without the execute bit also fails here. Check that Scanned Host OS matches the host.
Output not valid JSON
The script exited 0, but its output is not one JSON object with the types the table above gives. The detail gives the parser's error with its line and column. Common causes:
- A value containing an unescaped
"or\ - Extra text on standard output, such as a login banner or a message from
/etc/profileor~/.bashrc - A number in quotes for
speed_bps, or a status value in the wrong case - No output at all, from a script that does other work and reports nothing. Print
{}
Run the script with a command from Testing the script to see the exact output.
Output too large
The script printed more than 64 KiB. Scanopy stops reading at that limit and applies nothing. Print only the keys in the table, and limit interfaces to the interfaces you need.
Keys listed under Not applied
The run applied every other key and skipped these. A key appears here when it is not in the table above, such as interfaces[0].vlan, or when an interface mac is not a valid MAC address. Rename the key to one from the table or remove it.
Credential set up for another OS
The credential reads its key, password or script from a file on the daemon host, and the daemon runs a different OS from the credential's Daemon OS, so the daemon skipped the credential. Set Daemon OS to the daemon's OS, or scan with a daemon on the OS the credential names.
For credential loading problems, such as unreadable files and the per-session assignment summary, see Credential troubleshooting.