diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..a8af997
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,21 @@
+name: Test v1 scanner
+
+on:
+ push:
+ branches: [main, v1-base-model]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Validate shell syntax
+ run: |
+ bash -n src/scripts/security-check.sh
+ bash -n tests/run-tests.sh
+ - name: Run fixture tests
+ run: bash tests/run-tests.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..08d3174
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog
+
+## 1.0.0 - 2026-09-04
+
+Initial working base model.
+
+- Replaced the placeholder Vue UI with a traditional Unraid WebGUI `.page` implementation.
+- Added a read-only scanner that emits structured JSON.
+- Added checks for root account protection, Telnet, FTP, SSH authentication, public SMB shares, flash-share exposure, public NFS exports, WebGUI TLS, common management listeners, and installed Unraid version.
+- Explicitly distinguishes local listeners from proven Internet exposure.
+- Added fixture-based tests for insecure and secure configurations.
+- Reworked the plugin installer so runtime source is installed from an immutable pinned Git commit rather than executing whatever happens to be on `main` at install time.
diff --git a/PROJECT-STATUS.md b/PROJECT-STATUS.md
index a02eb13..f93ed3c 100644
--- a/PROJECT-STATUS.md
+++ b/PROJECT-STATUS.md
@@ -1,603 +1,118 @@
-# Unraid Security — Project Status and Development Roadmap
+# Unraid Security — Project Status
-**Status:** Early development / prototype
-**Production ready:** No
-**Primary goal:** Build a read-only Unraid security auditor that identifies meaningful configuration risks without automatically changing the system.
+**Current version:** 1.0.0
+**Status:** Working base model / early testing
+**Safety model:** Read-only
+**Minimum declared Unraid version:** 6.12.0
-## Project Vision
+## v1 status
-`unraid-security` is intended to provide Unraid users with a simple security dashboard that evaluates common security and hardening concerns and presents the results in understandable terms.
+The repository is no longer just an installation scaffold. v1 contains a functioning Bash scanner and a traditional Unraid WebGUI `.page` dashboard.
-The goal is not to turn Unraid into a generic enterprise Linux server or blindly apply security benchmarks designed for other platforms.
+The scanner emits structured JSON. The WebGUI renders each finding with its status, severity, observed state, reason, and recommended next step. The plugin does not automatically change any Unraid setting.
-The goal is to answer a much more practical question:
+## Implemented checks
-> **Is this Unraid server configured in a reasonably secure way, and if not, what should the owner look at?**
+v1 checks:
-The plugin should focus on Unraid-specific risks, explain why each finding matters, and provide useful remediation guidance.
+1. root account password protection;
+2. Telnet listener state;
+3. traditional FTP listener state;
+4. SSH listener state;
+5. SSH `PermitEmptyPasswords`;
+6. SSH `PasswordAuthentication`;
+7. exported SMB user shares configured as Public;
+8. boot/flash SMB share exposure and security mode;
+9. public NFS user-share exports;
+10. WebGUI SSL/TLS mode;
+11. common local management listeners;
+12. installed Unraid version.
-For the initial versions, the plugin should remain **read-only**. It should identify problems, not automatically modify system configuration.
+The installed-version check is informational. v1 does not contact Unraid and therefore does not claim to know whether the installed version is current.
----
+## Important boundary: local listener vs. WAN exposure
-## Current State
+v1 does **not** call a locally listening service "Internet exposed."
-The repository is currently a **working installation scaffold**, not a functioning security scanner.
+For example, SSH listening on `0.0.0.0:22` means the Unraid host is accepting connections on local interfaces. It does not prove that the router forwards TCP/22 from the public Internet.
-The existing structure proves that the plugin can be installed and that files can be placed into the Unraid WebGUI plugin directory, but the advertised security functionality has not yet been implemented.
+WAN exposure can depend on router forwarding, UPnP/NAT-PMP, reverse proxies, VPN or mesh networking, Unraid remote-access configuration, and upstream firewall rules. A future external validation feature should remain separate from the local scanner.
-| Component | Current state |
-| --- | --- |
-| Plugin installer | Prototype works |
-| Security scanner | Stub |
-| Security dashboard | Placeholder |
-| SSH assessment | Not implemented |
-| Share assessment | Not implemented |
-| Account/password assessment | Not implemented |
-| WAN exposure assessment | Not implemented |
-| Remediation guidance | Not implemented |
-| Automatic remediation | Intentionally not implemented |
-| Background daemon | None |
+## Architecture
-The current scanner at `src/scripts/security-check.sh` only reports that the security check is a stub and exits successfully.
+### Scanner
-The current WebGUI page at `src/unraid-security.page.txt` is likewise explicitly marked as a placeholder.
-
-The metadata at `src/plugin.json.txt` also describes the project as a placeholder used for testing the installation structure.
-
-This is therefore best understood as **the foundation for the intended plugin**, rather than a usable security product today.
-
----
-
-## Security Review of the Current Installer
-
-### 1. Executable code is downloaded from `main`
-
-The most important issue in the existing installation design is in `unraid-security.plg`.
-
-The installer currently downloads plugin files directly from the repository's `main` branch.
-
-That includes the executable security-check script.
-
-Conceptually, the installer does this:
-
-```text
-released PLG
- ↓
-downloads current contents of main
- ↓
-installs those files as the plugin
-```
-
-This means a versioned plugin does not actually install an immutable version of the source.
-
-For example, a PLG identifying itself as version `2025.06.25` may install different code depending on what happens to be present on `main` when the installation occurs.
-
-That creates an unnecessary supply-chain risk, particularly because an Unraid plugin can operate with significant access to the host system.
-
-### Recommended design
-
-Released versions should install an immutable release artifact.
-
-```text
-source
- ↓
-versioned release
- ↓
-unraid-security-x.y.z.txz
- ↓
-checksum verification
- ↓
-Unraid installation
-```
-
-A released PLG should never fetch executable code from a mutable development branch.
-
-At minimum:
-
-- package the plugin into a versioned release artifact;
-- pin the installer to that artifact;
-- publish a cryptographic checksum;
-- verify the package before installation;
-- ensure a previously released version cannot silently change when `main` changes.
-
----
-
-## WebGUI Architecture
-
-The current `unraid-security.page.txt` file resembles a Vue single-file component:
-
-```html
-
-...
-
-
-
-```
-
-That should not be assumed to work as a conventional Unraid WebGUI `.page` plugin simply because it is placed under:
-
-```text
-/usr/local/emhttp/plugins/unraid-security/
-```
-
-Before further UI development, the project should make an explicit architecture decision.
-
-The two questions are:
-
-1. Is the plugin targeting the traditional Unraid `emhttp` WebGUI plugin architecture?
-2. Or is it targeting the newer Unraid UI/API architecture?
-
-The current repository risks mixing assumptions from the two.
-
-The first meaningful development milestone should establish the supported Unraid architecture and build the UI correctly for that target.
-
----
-
-## Security Scanner Philosophy
-
-The scanner should begin with three rules.
-
-### 1. Read-only by default
-
-The first production release should inspect configuration but not change it.
-
-A finding should tell the user:
-
-- what was observed;
-- whether it is good, questionable, or dangerous;
-- why it matters;
-- what the user can do about it.
-
-It should not begin rewriting SSH, Samba, networking, or authentication configuration.
-
-Automatic remediation can be considered later, after the detection logic is mature and well tested.
-
-### 2. Unraid-specific recommendations
-
-A generic Linux hardening benchmark is not enough.
-
-Unraid has its own architecture, management model, storage model, WebGUI, shares, Docker integration, remote-access mechanisms, and administrative conventions.
-
-A configuration that looks strange to a generic Linux security scanner may be perfectly normal for Unraid.
-
-Every check should therefore answer:
-
-> Is this actually a security problem on Unraid?
-
-rather than:
-
-> Does this look different from a hardened Ubuntu server?
-
-### 3. Avoid false certainty
-
-The plugin should clearly distinguish among:
-
-- something directly observed on the server;
-- something inferred from configuration;
-- something the plugin cannot determine locally.
-
-This is especially important for network exposure.
-
----
-
-## Proposed Finding Model
-
-Every check should return structured data rather than simply printing text.
-
-For example:
-
-```json
-{
- "id": "ssh-password-auth",
- "category": "ssh",
- "title": "SSH password authentication",
- "status": "warning",
- "severity": "medium",
- "observed": "PasswordAuthentication yes",
- "recommended": "Use SSH keys where practical",
- "reason": "Password authentication increases exposure to credential attacks."
-}
-```
-
-A common schema makes it possible for the scanner and UI to remain separate.
-
-The backend determines facts.
-
-The UI decides how to display them.
-
----
-
-## Proposed Scanner Architecture
-
-Rather than allowing one large shell script to accumulate every check, security checks should be modular.
-
-A possible structure:
-
-```text
-src/
-└── scripts/
- ├── security-check.sh
- └── checks/
- ├── accounts.sh
- ├── ssh.sh
- ├── shares.sh
- ├── network.sh
- ├── services.sh
- ├── webgui.sh
- ├── docker.sh
- ├── updates.sh
- └── filesystem.sh
-```
-
-`security-check.sh` acts as the orchestrator.
-
-Individual modules perform narrowly scoped checks and return normalized findings.
-
-This provides several benefits:
-
-- checks can be tested independently;
-- new checks can be added without rewriting the scanner;
-- failures in one category do not need to break the entire scan;
-- severity and output formatting can remain consistent;
-- the WebGUI does not need to know how each finding was discovered.
-
----
-
-## Initial Security Checks
-
-Version 0.1 should resist the temptation to scan everything.
-
-A small number of defensible checks is more valuable than dozens of noisy ones.
-
-A reasonable first release would include approximately the following.
-
-### Accounts
-
-#### Root account protection
-
-Determine whether the administrative account has appropriate authentication protection.
-
-The scanner can safely detect conditions such as an absent password or unexpected account state.
-
-It should **not** claim to know whether a password is "weak" when it does not possess the plaintext password.
-
-For that reason, the existing concept of **weak password detection** should become something more accurate such as:
-
-> **Root account protection**
-
----
-
-## SSH
-
-Potential checks include:
-
-- `PermitEmptyPasswords`
-- `PasswordAuthentication`
-- `PubkeyAuthentication`
-- SSH listening state
-- SSH listening interfaces
-- SSH port
-- relevant root-login configuration
-
-Findings must account for the fact that Unraid's administrative model differs from that of many conventional Linux servers.
-
-The scanner should report what it sees and evaluate it in the context of Unraid rather than blindly importing generic Linux recommendations.
-
----
-
-## Shares
-
-Share security should be one of the project's highest-value areas.
-
-Potential findings include:
-
-- publicly writable SMB shares;
-- publicly readable shares;
-- Secure versus Private share configuration;
-- flash/boot-device share exposure;
-- NFS exports;
-- unexpectedly broad export permissions.
-
-Severity should reflect the actual configuration rather than treating all exported shares as equally dangerous.
-
-For example:
-
-```text
-Public read/write share HIGH
-Public read-only share WARNING
-Private authenticated PASS
-```
-
-The exact severity model should be tested and documented before release.
-
----
-
-## Network Services
-
-The scanner should identify services that commonly deserve scrutiny on an Unraid host.
-
-Initial candidates include:
-
-- Telnet;
-- FTP;
-- SSH;
-- NFS;
-- WebGUI listeners;
-- other externally listening management services.
-
-Finding a listening service is useful information, but it is not sufficient evidence that the service is exposed to the Internet.
-
----
-
-## WAN Exposure
-
-WAN exposure is one of the most useful potential features of the project and also one of the easiest to implement incorrectly.
-
-A command such as:
-
-```bash
-ss -lntup
-```
-
-can determine what the Unraid host is listening to.
-
-It cannot, by itself, determine what the public Internet can reach.
-
-For example:
-
-```text
-0.0.0.0:22
-```
-
-means SSH is listening on local interfaces.
-
-It does **not** prove that TCP/22 has been forwarded through the user's router.
-
-The project should therefore distinguish between:
-
-### Local listener
-
-A service is accepting connections on one or more interfaces.
-
-### Known or inferred remote exposure
-
-Local configuration provides evidence of a mechanism intended to make the service reachable remotely.
-
-### Externally verified exposure
-
-A service has actually been observed as reachable from outside the local network.
-
-The plugin should never report:
-
-> SSH is exposed to the Internet
-
-when the only evidence is that SSH is listening locally.
-
-Avoiding false positives is essential to making the security score trustworthy.
-
----
-
-## WebGUI Security
-
-Potential checks include:
-
-- HTTP versus HTTPS configuration;
-- management interface listening state;
-- remote-management settings;
-- unexpected interface exposure;
-- other security-relevant WebGUI configuration.
-
-These checks should be based on documented Unraid behavior and tested against supported Unraid versions.
-
----
-
-## Updates
-
-The scanner should report the state of the Unraid OS version when that information can be obtained reliably.
-
-The goal is not necessarily to demand the newest release immediately.
-
-Instead, it should identify whether:
-
-- the system appears current;
-- a newer supported release is available;
-- the running version has known support or security implications that warrant attention.
-
----
-
-## Proposed Dashboard
-
-The dashboard should emphasize findings rather than decoration.
-
-A useful first version might look conceptually like this:
+Runtime path:
```text
-UNRAID SECURITY
-────────────────────────────────────
-
-Overall assessment: Needs Attention
-
-CRITICAL
-✗ Telnet enabled
-
-HIGH
-✗ Public writable share: Media
-✗ Flash share exported over SMB
-
-MEDIUM
-⚠ SSH password authentication enabled
-⚠ HTTPS not enforced
-
-GOOD
-✓ Root account protected
-✓ NFS disabled
-✓ FTP disabled
-
-[View all findings]
+/usr/local/emhttp/plugins/unraid-security/scripts/security-check.sh
```
-Every finding should be expandable into something like:
+The scanner is Bash and has no daemon or persistent background process. It runs on demand when the WebGUI page is loaded or refreshed.
-```text
-Finding:
-SSH password authentication is enabled.
-
-Observed:
-PasswordAuthentication yes
-
-Why this matters:
-Password authentication increases exposure to
-credential guessing and stolen-password attacks.
-
-Recommendation:
-Consider SSH key authentication where practical.
-
-Automatic change:
-None. This plugin is read-only.
-```
+### WebGUI
-The important feature is not the score.
-
-The important feature is that the user understands the finding.
-
----
-
-## Security Scoring
-
-A numeric score may eventually be useful, but it should not be introduced until the severity model is defensible.
-
-A superficially precise score such as:
+Runtime page:
```text
-Security Score: 82/100
+/usr/local/emhttp/plugins/unraid-security/unraid-security.page
```
-is worse than no score if the weighting behind it is arbitrary.
+v1 uses the established Unraid `.page` format with PHP instead of the original placeholder Vue component. It is placed under the Settings/User Utilities area using `Menu="OtherSettings"`.
-Early versions should prefer:
+### Configuration reads
-```text
-Critical
-High
-Medium
-Informational
-Pass
-```
-
-Once enough checks exist and their relative importance has been validated, an aggregate score can be added.
-
----
-
-## Version 0.1 Target
-
-The first meaningful release should focus on approximately 10–12 checks.
-
-Suggested initial scope:
-
-1. Root account protection
-2. Telnet enabled/disabled
-3. FTP enabled/disabled
-4. SSH `PermitEmptyPasswords`
-5. SSH password-authentication state
-6. SSH listening state and interfaces
-7. Public writable shares
-8. Flash/boot share exposure
-9. NFS exports
-10. WebGUI HTTPS configuration
-11. Remote-management exposure indicators
-12. Unraid update status
-
-Success for version 0.1 should mean:
-
-> Every reported finding is something the project can explain and defend.
-
-Not:
-
-> The project has the largest possible number of checks.
-
----
-
-## Testing Requirements
-
-Security software must earn trust.
-
-Each check should have test fixtures representing at least:
-
-- known-good configuration;
-- known-bad configuration;
-- missing configuration;
-- malformed configuration;
-- unexpected but valid configuration.
-
-Where Unraid behavior differs among supported releases, version-specific tests should be added.
-
-The scanner should fail conservatively.
-
-If it cannot determine something reliably, the result should be:
+The scanner reads normal Unraid/Linux runtime state, including:
```text
-UNKNOWN
+/etc/shadow
+/etc/ssh/sshd_config
+/boot/config/shares/*.cfg
+/boot/config/flash.cfg
+/boot/config/ident.cfg
+/etc/unraid-version
```
-rather than manufacturing a PASS or FAIL.
-
----
-
-## Packaging Requirements
-
-Before the project is considered releasable:
-
-- executable code must not be pulled from `main`;
-- releases must be immutable;
-- packages should be versioned;
-- package integrity should be verifiable;
-- installation failures should stop safely;
-- uninstall should remove only files owned by the plugin;
-- supported Unraid versions should be explicitly tested;
-- the WebGUI implementation should use the architecture appropriate to those versions.
-
----
+It also inspects local TCP listeners using `ss` when available, with a `netstat` fallback.
-## What the Project Is Not
+## Packaging
-The plugin is not intended to be:
+The original prototype installer downloaded runtime files from mutable `main` during installation. v1 removes that behavior: the installer is generated against an exact Git commit and downloads runtime source from that immutable revision.
-- an antivirus product;
-- an intrusion-detection system;
-- a vulnerability scanner for every Docker container;
-- an Internet perimeter scanner;
-- an enterprise compliance platform;
-- an automatic hardening engine.
+This is a substantial improvement over executing whatever happens to be on `main`, but the mature distribution target remains a versioned release artifact with checksum verification.
-At least initially, its job is much simpler:
+## Testing status
-> **Look at an Unraid server the way a security-minded administrator would, identify meaningful configuration risks, and explain them clearly.**
+v1 includes fixture-based tests for intentionally insecure and secure configurations. The tests verify valid JSON output and the principal account, service, SSH, SMB, flash-share, NFS, TLS, and version findings.
-That is enough to make the project useful.
+The code has also been checked for Bash syntax, the WebGUI PHP has been linted, and the plugin XML is validated before merge.
----
+**The remaining gap is live testing on real Unraid systems.** v1 should therefore be treated as an early base release until it has been exercised on real 6.12 and 7.x servers with different configurations and themes.
-## Current Assessment
+## Security decisions
-The repository today is a prototype, not a security product.
+- **Read-only:** no automatic remediation.
+- **Password strength:** no cracking and no pretend strength inference from password hashes.
+- **Public SMB:** exported Public shares are treated as high risk because guest users receive read/write access.
+- **Boot device:** Public flash sharing is critical; other exported modes still receive warnings.
+- **WebGUI TLS:** v1 evaluates the local management setting but does not validate certificate trust, expiry, reverse-proxy TLS, or every access path.
+- **WAN:** local listeners are evidence of local service availability, not proof of public reachability.
-That is not a reason to abandon it.
+## Known limitations
-The existing installation work establishes a starting point. The next phase should shift away from placeholder UI work and toward three fundamentals:
+v1 is not antivirus, IDS/IPS, a CVE scanner, a Docker image scanner, an Internet perimeter scanner, a router auditor, or an automatic hardening engine.
-1. **Correct plugin architecture**
-2. **Secure, immutable packaging**
-3. **A small set of trustworthy read-only security checks**
+It also does not yet inspect Docker privilege/capability settings, deeply audit Unraid Connect/Tailscale, inspect every custom Samba configuration, or retain historical scan results.
-Once those exist, the dashboard becomes a way of presenting a real security model rather than a shell waiting for one.
+## Next priorities
-The core idea remains strong:
+The next work should be driven by real-server testing rather than by adding a large number of speculative checks:
-> Give ordinary Unraid administrators a clear answer to **"What on my server should I be worried about?"** without requiring them to become Linux security specialists first.
+1. install v1 on real Unraid 6.12 and 7.x systems;
+2. verify page placement and rendering across built-in themes;
+3. compare every finding against the WebGUI setting that produced it;
+4. fix false positives before adding checks;
+5. move distribution to immutable release assets with integrity verification;
+6. add carefully scoped remote-access and Docker-security checks.
-That is the product worth building.
+> **If the plugin cannot explain and defend a finding, it should not report it as fact.**
diff --git a/README.md b/README.md
index 6bbd8c5..d7a2ec0 100644
--- a/README.md
+++ b/README.md
@@ -1,136 +1,96 @@
# unraid-security
-A planned **read-only security auditor for Unraid** designed to help server owners identify meaningful configuration risks and understand what to do about them.
+A **read-only security configuration auditor for Unraid**. It checks a small set of high-value settings, explains what it found, and recommends what to review without changing the server.
> [!IMPORTANT]
-> **This project is currently an early-development prototype and is not production ready.**
-> The repository contains the plugin installation scaffold, but the security checks and dashboard are not yet implemented.
+> **v1.0.0 is a base working model.** The scanner and WebGUI are implemented and fixture-tested, but this release still needs broader testing on real Unraid 6.12/7.x systems before it should be treated as a mature security control.
-## Goal
+## What v1 checks
-The project is intended to answer a practical question:
+- root account password protection;
+- Telnet listener state;
+- traditional FTP listener state;
+- SSH listener state;
+- SSH `PermitEmptyPasswords`;
+- SSH `PasswordAuthentication`;
+- exported SMB shares using Unraid Public security mode;
+- boot/flash share SMB exposure;
+- public NFS user-share exports;
+- WebGUI SSL/TLS configuration;
+- common local management listeners;
+- installed Unraid version.
-> **Is this Unraid server configured in a reasonably secure way, and if not, what should the owner look at?**
+The scanner intentionally does **not** claim that a local listening port is Internet-accessible. Router port forwarding, UPnP, reverse proxies, and true external reachability are separate questions.
-Rather than blindly applying generic Linux hardening rules, `unraid-security` will focus on risks that make sense specifically for Unraid.
+## Installation
-The initial design is deliberately **read-only**: detect, explain, and recommend — but do not automatically rewrite system configuration.
+In Unraid, go to **Plugins → Install Plugin** and use:
-## Current Status
-
-Today, the repository is primarily an installation and UI scaffold.
-
-| Component | Status |
-| --- | --- |
-| Plugin installer | Prototype |
-| Security scanner | Stub |
-| Security dashboard | Placeholder |
-| SSH assessment | Planned |
-| Share assessment | Planned |
-| Account protection checks | Planned |
-| Network/service assessment | Planned |
-| WAN exposure assessment | Planned |
-| Remediation guidance | Planned |
-| Automatic remediation | Not planned for the initial release |
-
-The current `security-check.sh` script is only a stub, and the current WebGUI page is a placeholder.
-
-For the detailed technical review, architecture notes, security concerns, and development roadmap, see **[PROJECT-STATUS.md](PROJECT-STATUS.md)**.
-
-## Planned Initial Checks
-
-The first useful release is expected to focus on a small set of checks that can be explained and defended, including:
-
-- root account protection;
-- Telnet state;
-- FTP state;
-- SSH authentication settings;
-- SSH listening state and interfaces;
-- public writable shares;
-- flash/boot share exposure;
-- NFS exports;
-- WebGUI HTTPS configuration;
-- remote-management exposure indicators;
-- Unraid update status.
-
-The goal is not to maximize the number of checks. The goal is to minimize false positives and make every finding useful.
-
-## Design Principles
-
-### Read-only first
-
-The plugin should inspect and report before it ever considers changing configuration automatically.
-
-A useful finding should explain:
-
-- what was observed;
-- how serious it is;
-- why it matters;
-- what the user can do about it.
+```text
+https://raw.githubusercontent.com/SirSmirksALot/unraid-security/main/unraid-security.plg
+```
-### Unraid-specific
+After installation, open **Settings → User Utilities → Unraid Security** (menu placement can vary slightly by Unraid version/theme).
-Unraid is not a generic hardened Linux server. Recommendations need to account for Unraid's administration model, shares, WebGUI, Docker integration, networking, and remote-access mechanisms.
+## Design
-### No false certainty
+v1 is deliberately conservative:
-The scanner should distinguish between facts it can observe locally and things it cannot know.
+- **Read-only:** it does not rewrite SSH, Samba, NFS, account, or WebGUI settings.
+- **Unraid-specific:** checks are based on Unraid configuration rather than generic Linux-hardening assumptions.
+- **Explainable:** every finding includes the observed state, why it matters, and a recommendation.
+- **Structured:** the scanner emits JSON so the WebGUI is separate from detection logic.
+- **No false certainty:** `unknown` is preferred over inventing a pass/fail result.
-For example, a service listening on `0.0.0.0:22` does **not** by itself prove that SSH is exposed to the public Internet. Router configuration and external reachability are separate questions.
+The scanner lives at:
-### Structured findings
+```text
+/usr/local/emhttp/plugins/unraid-security/scripts/security-check.sh
+```
-Security checks should eventually return normalized results that the UI can consume independently, for example:
+and emits output shaped like:
```json
{
- "id": "ssh-password-auth",
- "category": "ssh",
- "title": "SSH password authentication",
- "status": "warning",
- "severity": "medium",
- "observed": "PasswordAuthentication yes",
- "recommended": "Use SSH keys where practical",
- "reason": "Password authentication increases exposure to credential attacks."
+ "scanner_version": "1.0.0",
+ "generated_at": "2026-09-04T18:00:00Z",
+ "findings": [
+ {
+ "id": "ssh-password-auth",
+ "category": "ssh",
+ "title": "SSH password authentication",
+ "status": "warning",
+ "severity": "medium",
+ "observed": "PasswordAuthentication yes",
+ "reason": "...",
+ "recommendation": "..."
+ }
+ ]
}
```
-## Roadmap
-
-The next major milestones are:
-
-1. establish the correct Unraid WebGUI/plugin architecture;
-2. replace mutable `main`-branch installation with immutable, versioned release packaging and integrity verification;
-3. implement the first set of read-only security checks;
-4. add test fixtures for known-good, known-bad, missing, and malformed configurations;
-5. build the dashboard around real scanner output;
-6. validate behavior across supported Unraid versions.
+## Development
-See **[PROJECT-STATUS.md](PROJECT-STATUS.md)** for the full roadmap.
+Run the fixture tests on a Linux development machine with Bash and Python 3:
-## What This Project Is Not
-
-At least initially, `unraid-security` is not intended to be:
-
-- antivirus software;
-- an intrusion-detection system;
-- a vulnerability scanner for every Docker container;
-- an Internet perimeter scanner;
-- an enterprise compliance product;
-- an automatic hardening engine.
-
-Its job is simpler:
-
-> **Look at an Unraid server the way a security-minded administrator would, identify meaningful configuration risks, and explain them clearly.**
+```bash
+bash tests/run-tests.sh
+```
-## Contributing
+The scanner supports path overrides used by the test harness, allowing Unraid-like configuration trees to be tested without modifying the development system.
-The project is still defining its security model and architecture. Issues and contributions that improve Unraid-specific detection accuracy, reduce false positives, strengthen packaging, or add well-tested checks are welcome.
+## v1 limitations
-Security findings should be based on observable behavior and documented Unraid configuration rather than generic assumptions wherever possible.
+- no automatic remediation;
+- no external/WAN port scan;
+- no router or firewall inspection;
+- no Docker-container vulnerability scanning;
+- no package/CVE database integration;
+- no password cracking or password-strength inference from hashes;
+- update check reports the installed Unraid version but does not contact Unraid to determine whether a newer release exists.
-## Development Status
+For the design rationale and longer roadmap, see **[PROJECT-STATUS.md](PROJECT-STATUS.md)**.
-Expect breaking changes while the project is in prototype development.
+## Safety model
-Do not rely on the current repository as a production security control.
+This project should remain boring in the best possible way: inspect, report, explain. A security plugin that changes a NAS automatically can create more risk than it removes, so remediation is intentionally outside the v1 scope.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..3eefcb9
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/src/include/style.css b/src/include/style.css
deleted file mode 100644
index b83ea66..0000000
--- a/src/include/style.css
+++ /dev/null
@@ -1,5 +0,0 @@
-body {
- background-color: #1d1d1d;
- color: #ddd;
- font-family: sans-serif;
-}
diff --git a/src/plugin.json.txt b/src/plugin.json.txt
index f770623..0d6f73d 100644
--- a/src/plugin.json.txt
+++ b/src/plugin.json.txt
@@ -1,8 +1,8 @@
{
"name": "unraid-security",
- "version": "2025.06.25",
+ "version": "1.0.0",
"author": "Sean Knight",
- "category": "Security",
- "min": "6.12.0",
- "description": "Placeholder plugin for testing installation structure."
+ "description": "Read-only Unraid security configuration auditor",
+ "min_unraid": "6.12.0",
+ "status": "v1 base model"
}
diff --git a/src/scripts/security-check.sh b/src/scripts/security-check.sh
index eb91ba4..58f3c14 100644
--- a/src/scripts/security-check.sh
+++ b/src/scripts/security-check.sh
@@ -1,3 +1,256 @@
#!/bin/bash
-echo "Running security check... (stub)"
-exit 0
+set -u
+
+VERSION="1.0.0"
+ROOT="${UNRAID_SECURITY_ROOT:-}"
+
+path() {
+ local p="$1"
+ if [[ -n "$ROOT" ]]; then
+ printf '%s%s' "${ROOT%/}" "$p"
+ else
+ printf '%s' "$p"
+ fi
+}
+
+SHADOW_FILE="${UNRAID_SECURITY_SHADOW_FILE:-$(path /etc/shadow)}"
+SSHD_CONFIG="${UNRAID_SECURITY_SSHD_CONFIG:-$(path /etc/ssh/sshd_config)}"
+SHARE_DIR="${UNRAID_SECURITY_SHARE_DIR:-$(path /boot/config/shares)}"
+FLASH_CFG="${UNRAID_SECURITY_FLASH_CFG:-$(path /boot/config/flash.cfg)}"
+IDENT_CFG="${UNRAID_SECURITY_IDENT_CFG:-$(path /boot/config/ident.cfg)}"
+UNRAID_VERSION_FILE="${UNRAID_SECURITY_VERSION_FILE:-$(path /etc/unraid-version)}"
+LISTENERS_FILE="${UNRAID_SECURITY_LISTENERS_FILE:-}"
+SSHD_EFFECTIVE_FILE="${UNRAID_SECURITY_SSHD_EFFECTIVE_FILE:-}"
+
+FINDINGS=()
+
+json_escape() {
+ local s="${1-}"
+ s=${s//\\/\\\\}
+ s=${s//\"/\\\"}
+ s=${s//$'\n'/\\n}
+ s=${s//$'\r'/\\r}
+ s=${s//$'\t'/\\t}
+ printf '%s' "$s"
+}
+
+add_finding() {
+ local id="$1" category="$2" title="$3" status="$4" severity="$5" observed="$6" reason="$7" recommendation="$8"
+ FINDINGS+=("{\"id\":\"$(json_escape "$id")\",\"category\":\"$(json_escape "$category")\",\"title\":\"$(json_escape "$title")\",\"status\":\"$(json_escape "$status")\",\"severity\":\"$(json_escape "$severity")\",\"observed\":\"$(json_escape "$observed")\",\"reason\":\"$(json_escape "$reason")\",\"recommendation\":\"$(json_escape "$recommendation")\"}")
+}
+
+cfg_value() {
+ local file="$1" key="$2"
+ [[ -r "$file" ]] || return 1
+ awk -F= -v key="$key" '
+ $1 == key {
+ value=substr($0,index($0,"=")+1)
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
+ if (value ~ /^".*"$/) { sub(/^"/, "", value); sub(/"$/, "", value) }
+ print value
+ exit
+ }
+ ' "$file"
+}
+
+listeners() {
+ if [[ -n "$LISTENERS_FILE" && -r "$LISTENERS_FILE" ]]; then
+ cat "$LISTENERS_FILE"
+ elif command -v ss >/dev/null 2>&1; then
+ ss -H -lntp 2>/dev/null || true
+ elif command -v netstat >/dev/null 2>&1; then
+ netstat -lntp 2>/dev/null | tail -n +3 || true
+ fi
+}
+
+port_listening() {
+ local port="$1"
+ listeners | grep -Eq "(^|[[:space:]])([^[:space:]]*):${port}([[:space:]]|$)"
+}
+
+sshd_effective() {
+ if [[ -n "$SSHD_EFFECTIVE_FILE" && -r "$SSHD_EFFECTIVE_FILE" ]]; then
+ cat "$SSHD_EFFECTIVE_FILE"
+ elif command -v sshd >/dev/null 2>&1; then
+ sshd -T 2>/dev/null || true
+ elif [[ -r "$SSHD_CONFIG" ]]; then
+ awk '
+ /^[[:space:]]*#/ {next}
+ NF >= 2 {key=tolower($1); $1=""; sub(/^[[:space:]]+/, ""); print key " " tolower($0)}
+ ' "$SSHD_CONFIG"
+ fi
+}
+
+sshd_value() {
+ local key="$1"
+ sshd_effective | awk -v key="$key" 'tolower($1)==tolower(key){print tolower($2); exit}'
+}
+
+# 1. Root account protection
+if [[ ! -r "$SHADOW_FILE" ]]; then
+ add_finding "root-account" "accounts" "Root account protection" "unknown" "high" "Could not read /etc/shadow" "The scanner could not determine whether the root account has password protection." "Review the root account under Users and ensure it is protected with a strong password or intentionally locked."
+else
+ root_hash="$(awk -F: '$1=="root"{print $2; exit}' "$SHADOW_FILE")"
+ if [[ -z "$root_hash" ]]; then
+ add_finding "root-account" "accounts" "Root account protection" "fail" "critical" "Root password field is empty" "An empty root password can allow administrative access without a password, depending on the authentication path." "Set a strong root password immediately."
+ elif [[ "$root_hash" == '!'* || "$root_hash" == '*'* ]]; then
+ add_finding "root-account" "accounts" "Root account protection" "pass" "none" "Root password authentication appears locked" "A locked password field prevents password-based authentication for the root account." "No action required unless the account was locked unintentionally."
+ else
+ add_finding "root-account" "accounts" "Root account protection" "pass" "none" "Root password hash is present" "The root account has a password hash. This check cannot determine password strength from the hash." "Use a unique, strong root password and do not reuse it elsewhere."
+ fi
+fi
+
+# 2. Telnet
+if port_listening 23; then
+ add_finding "telnet-listener" "services" "Telnet service" "fail" "critical" "TCP port 23 is listening" "Telnet transmits credentials and session data without encryption." "Disable Telnet and use SSH for command-line administration."
+else
+ add_finding "telnet-listener" "services" "Telnet service" "pass" "none" "No TCP listener detected on port 23" "No Telnet listener was detected locally." "No action required."
+fi
+
+# 3. FTP
+if port_listening 21; then
+ add_finding "ftp-listener" "services" "FTP service" "warning" "high" "TCP port 21 is listening" "Traditional FTP does not encrypt credentials or file transfers." "Disable FTP unless it is explicitly required, and prefer an encrypted transfer method."
+else
+ add_finding "ftp-listener" "services" "FTP service" "pass" "none" "No TCP listener detected on port 21" "No traditional FTP listener was detected locally." "No action required."
+fi
+
+# 4-6. SSH
+ssh_port="$(sshd_value port)"
+[[ "$ssh_port" =~ ^[0-9]+$ ]] || ssh_port="22"
+if port_listening "$ssh_port"; then
+ add_finding "ssh-listener" "ssh" "SSH listener" "info" "info" "SSH is listening on TCP port $ssh_port" "A local SSH listener is not proof of Internet exposure, but it increases the services available on the server." "Keep SSH disabled when not needed, or restrict access to trusted networks and users."
+else
+ add_finding "ssh-listener" "ssh" "SSH listener" "pass" "none" "No SSH listener detected on configured port $ssh_port" "The scanner did not detect SSH listening on its configured port." "No action required."
+fi
+
+permit_empty="$(sshd_value permitemptypasswords)"
+case "$permit_empty" in
+ yes)
+ add_finding "ssh-empty-passwords" "ssh" "SSH empty passwords" "fail" "critical" "PermitEmptyPasswords yes" "SSH is configured to allow accounts with empty passwords to authenticate." "Set PermitEmptyPasswords to no and ensure every interactive account is protected."
+ ;;
+ no)
+ add_finding "ssh-empty-passwords" "ssh" "SSH empty passwords" "pass" "none" "PermitEmptyPasswords no" "SSH rejects empty-password authentication." "No action required."
+ ;;
+ *)
+ add_finding "ssh-empty-passwords" "ssh" "SSH empty passwords" "unknown" "high" "Could not determine PermitEmptyPasswords" "The scanner could not determine the effective SSH setting." "Review the effective sshd configuration and ensure PermitEmptyPasswords is disabled."
+ ;;
+esac
+
+password_auth="$(sshd_value passwordauthentication)"
+case "$password_auth" in
+ yes)
+ add_finding "ssh-password-auth" "ssh" "SSH password authentication" "warning" "medium" "PasswordAuthentication yes" "Password authentication is valid on Unraid but exposes SSH to password guessing and stolen-password risk when reachable." "Prefer SSH keys where practical and restrict SSH reachability to trusted networks."
+ ;;
+ no)
+ add_finding "ssh-password-auth" "ssh" "SSH password authentication" "pass" "none" "PasswordAuthentication no" "SSH password authentication is disabled." "No action required."
+ ;;
+ *)
+ add_finding "ssh-password-auth" "ssh" "SSH password authentication" "unknown" "medium" "Could not determine PasswordAuthentication" "The scanner could not determine the effective SSH authentication setting." "Review the effective sshd configuration."
+ ;;
+esac
+
+# 7. SMB user shares
+public_smb=()
+if [[ -d "$SHARE_DIR" ]]; then
+ shopt -s nullglob
+ for share_file in "$SHARE_DIR"/*.cfg; do
+ export_mode="$(cfg_value "$share_file" shareExport || true)"
+ security_mode="$(cfg_value "$share_file" shareSecurity || true)"
+ if [[ "$export_mode" == e* && "$security_mode" == "public" ]]; then
+ public_smb+=("$(basename "$share_file" .cfg)")
+ fi
+ done
+ shopt -u nullglob
+fi
+
+if (( ${#public_smb[@]} > 0 )); then
+ share_list="$(IFS=', '; echo "${public_smb[*]}")"
+ add_finding "public-smb-shares" "shares" "Public writable SMB shares" "fail" "high" "$share_list" "Unraid Public SMB mode grants guest users full read/write access to an exported share." "Change sensitive shares to Secure or Private and grant access only to users who need it."
+else
+ add_finding "public-smb-shares" "shares" "Public writable SMB shares" "pass" "none" "No exported Public SMB user shares detected" "No user share configuration was found that combines SMB export with Public security mode." "No action required."
+fi
+
+# 8. Boot/flash SMB exposure
+flash_export="$(cfg_value "$FLASH_CFG" flashExport || true)"
+flash_security="$(cfg_value "$FLASH_CFG" flashSecurity || true)"
+if [[ "$flash_export" == e* ]]; then
+ case "$flash_security" in
+ public)
+ add_finding "flash-share" "shares" "Boot device share" "fail" "critical" "Flash share is exported with Public security" "Public SMB mode grants guest users full read/write access to the boot device, which contains Unraid configuration and sensitive data." "Disable the flash share, or change it to Private and grant access only to trusted users."
+ ;;
+ secure)
+ add_finding "flash-share" "shares" "Boot device share" "warning" "high" "Flash share is exported with Secure security" "Secure mode still permits guest read access. The boot device contains configuration and other sensitive data." "Disable the flash share when not needed. If it must be shared, prefer Private access for trusted users."
+ ;;
+ private)
+ add_finding "flash-share" "shares" "Boot device share" "warning" "medium" "Flash share is exported with Private security" "Even restricted network exposure increases the impact of a trusted-user credential compromise." "Disable the flash share when not needed; otherwise keep it Private and tightly restrict users."
+ ;;
+ *)
+ add_finding "flash-share" "shares" "Boot device share" "warning" "high" "Flash share is exported${flash_security:+ with $flash_security security}" "The boot device is available over SMB, but the scanner could not classify the security mode reliably." "Review the flash share and disable network export unless it is required."
+ ;;
+ esac
+else
+ add_finding "flash-share" "shares" "Boot device share" "pass" "none" "Flash share is not exported over SMB" "The boot device is not exposed as an SMB share." "No action required."
+fi
+
+# 9. NFS exports
+public_nfs=()
+if [[ -d "$SHARE_DIR" ]]; then
+ shopt -s nullglob
+ for share_file in "$SHARE_DIR"/*.cfg; do
+ export_mode="$(cfg_value "$share_file" shareExportNFS || true)"
+ security_mode="$(cfg_value "$share_file" shareSecurityNFS || true)"
+ if [[ "$export_mode" == e* && "$security_mode" == "public" ]]; then
+ public_nfs+=("$(basename "$share_file" .cfg)")
+ fi
+ done
+ shopt -u nullglob
+fi
+
+if (( ${#public_nfs[@]} > 0 )); then
+ nfs_list="$(IFS=', '; echo "${public_nfs[*]}")"
+ add_finding "public-nfs-shares" "shares" "Public NFS exports" "warning" "high" "$nfs_list" "Public NFS exports can make data broadly available to systems permitted by the export configuration." "Restrict NFS exports to trusted hosts/networks and use the narrowest access mode that meets the need."
+else
+ add_finding "public-nfs-shares" "shares" "Public NFS exports" "pass" "none" "No Public NFS user-share exports detected" "No user share configuration was found that combines NFS export with Public security mode." "No action required."
+fi
+
+# 10. WebGUI HTTPS/TLS
+use_ssl="$(cfg_value "$IDENT_CFG" USE_SSL || true)"
+case "${use_ssl,,}" in
+ yes|strict|auto)
+ add_finding "webgui-tls" "webgui" "WebGUI TLS" "pass" "none" "USE_SSL=${use_ssl}" "Unraid is configured to use TLS for WebGUI access according to the local management setting." "No action required."
+ ;;
+ no|"")
+ add_finding "webgui-tls" "webgui" "WebGUI TLS" "warning" "medium" "USE_SSL=${use_ssl:-not set}" "Primary WebGUI access may use unencrypted HTTP, allowing credentials and session data to be exposed on an untrusted network." "Enable SSL/TLS under Settings → Management Access when practical."
+ ;;
+ *)
+ add_finding "webgui-tls" "webgui" "WebGUI TLS" "unknown" "medium" "USE_SSL=${use_ssl}" "The scanner does not recognize this TLS mode." "Review Settings → Management Access and verify that WebGUI traffic is encrypted."
+ ;;
+esac
+
+# 11. Local management listeners summary
+mgmt_lines="$(listeners | grep -E ':(22|23|80|443)([[:space:]]|$)' | head -n 8 || true)"
+if [[ -n "$mgmt_lines" ]]; then
+ add_finding "management-listeners" "network" "Management listeners" "info" "info" "$mgmt_lines" "These are local listeners only. They do not prove that any service is reachable from the public Internet." "Review router port-forwarding, UPnP, reverse proxies, and remote-access settings separately."
+else
+ add_finding "management-listeners" "network" "Management listeners" "info" "info" "No common management ports detected in local listener output" "This check only summarizes common local management listeners." "No action required."
+fi
+
+# 12. Installed Unraid version
+unraid_version=""
+if [[ -r "$UNRAID_VERSION_FILE" ]]; then
+ unraid_version="$(cfg_value "$UNRAID_VERSION_FILE" version || true)"
+ [[ -n "$unraid_version" ]] || unraid_version="$(grep -Eo '[0-9]+\.[0-9]+(\.[0-9]+)?[^[:space:]]*' "$UNRAID_VERSION_FILE" | head -n 1 || true)"
+fi
+if [[ -n "$unraid_version" ]]; then
+ add_finding "unraid-version" "updates" "Installed Unraid version" "info" "info" "$unraid_version" "This v1 scanner reports the installed version but does not contact Unraid to determine whether an update is available." "Use Tools → Update OS to check for supported updates and review release notes before upgrading."
+else
+ add_finding "unraid-version" "updates" "Installed Unraid version" "unknown" "info" "Could not determine installed Unraid version" "The version file was unavailable or did not contain a recognizable version." "Check /etc/unraid-version or Tools → Update OS."
+fi
+
+# Output
+printf '{"scanner_version":"%s","generated_at":"%s","findings":[' "$VERSION" "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+for i in "${!FINDINGS[@]}"; do
+ (( i > 0 )) && printf ','
+ printf '%s' "${FINDINGS[$i]}"
+done
+printf ']}\n'
diff --git a/src/unraid-security.page.txt b/src/unraid-security.page.txt
index 1c4f579..ad4a37f 100644
--- a/src/unraid-security.page.txt
+++ b/src/unraid-security.page.txt
@@ -1,25 +1,126 @@
-
- This is the placeholder UI for the Unraid Security plugin.Unraid Security
-
=us_e($script)?> exists and is executable.
+ =us_e($finding['observed'] ?? '')?>