Compare commits

..
4 Commits
12 changed files with 30 additions and 210 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
- name: Set the telemetry key
shell: bash
run: echo "FIREWALL_POSTHOG_KEY=${{ secrets.FIREWALL_POSTHOG_KEY }}" >> "$GITHUB_ENV"
-1
View File
@@ -13,7 +13,6 @@ jobs:
env:
TEAM: ${{ vars.APPLE_TEAM }}
NOTARY_PROFILE: firewall
FIREWALL_POSTHOG_KEY: ${{ secrets.FIREWALL_POSTHOG_KEY }}
steps:
- uses: actions/checkout@v6
with:
-40
View File
@@ -1,40 +0,0 @@
name: Rewrite release notes
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
notes:
runs-on: ubuntu-22.04
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ fromJson(inputs.plan).announcement_tag }}
steps:
- run: |
url="https://github.com/${{ github.repository }}/releases/download/$TAG"
cat > notes.md <<NOTES
## macOS
[Firewall.pkg]($url/Firewall.pkg)
## Windows
[strix-firewall-x86_64-pc-windows-msvc.msi]($url/strix-firewall-x86_64-pc-windows-msvc.msi)
## Command line
**Command-line installs do not include machine-wide screening.**
\`\`\`sh
curl --proto '=https' --tlsv1.2 -LsSf $url/strix-firewall-installer.sh | sh
\`\`\`
\`\`\`sh
powershell -ExecutionPolicy Bypass -c "irm $url/strix-firewall-installer.ps1 | iex"
\`\`\`
NOTES
gh release edit "$TAG" --repo "${{ github.repository }}" --notes-file notes.md
+2 -13
View File
@@ -127,9 +127,6 @@ jobs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: "Set the telemetry key"
run: "echo \"FIREWALL_POSTHOG_KEY=${{ secrets.FIREWALL_POSTHOG_KEY }}\" >> \"$GITHUB_ENV\""
shell: "bash"
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
@@ -302,7 +299,8 @@ jobs:
secrets: inherit
# publish jobs get escalated permissions
permissions:
"contents": "read"
"id-token": "write"
"packages": "write"
announce:
needs:
@@ -321,12 +319,3 @@ jobs:
with:
persist-credentials: false
submodules: recursive
custom-release-notes:
needs:
- plan
- announce
uses: ./.github/workflows/release-notes.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
-95
View File
@@ -1,95 +0,0 @@
# Strix's Dependency Firewall
Stops malicious packages before they install. Built by [Strix](https://strix.ai): read [the blogpost](https://www.strix.ai/blog/firewall-announcement).
Screening runs on your device: downloads go straight to the registry, never through a Strix server.
Each package's name and version are looked up [Strix's malicious-package blocklist](https://github.com/usestrix/firewall-blocklist), so installs are blocked system-wide *before* reaching your machine.
https://github.com/user-attachments/assets/835411b1-5c89-409f-b98a-cb238310429c
## Install
**Machine-wide**, from the [latest release](https://github.com/usestrix/firewall/releases/latest):
- macOS: open `Firewall.pkg`, then approve the extension when macOS asks.
- Windows: run the `.msi` as an admin.
**Prefix-mode**
cargo:
```sh
cargo install strix-firewall
```
curl:
```sh
curl -LsSf https://github.com/usestrix/firewall/releases/latest/download/strix-firewall-installer.sh | sh
```
Powershell:
```powershell
powershell -c "irm https://github.com/usestrix/firewall/releases/latest/download/strix-firewall-installer.ps1 | iex"
```
## Platforms
| | Machine-wide | Command prefix |
|---|---|---|
| macOS 13+ | ✅ Apple silicon or T2 chip | ✅ |
| Windows 10/11, x86-64 | ✅ with a TPM 2.0 | ✅ |
| Linux (glibc 2.35+), x86-64 or arm64 | 🚧 Coming Soon | ✅ |
## Supported registries
| Registry | Package managers |
|---|---|
| npm | npm, pnpm, bun |
| PyPI | pip, uv, pdm, pipenv |
| crates.io | cargo |
| Go module proxy | go |
| RubyGems | gem |
Mirrors of these, such as Artifactory or Nexus, can be added in via a config file.
## Features
- **Up-to-the-minute data:** Strix constantly monitors updates to top packages (covering 99% of installed packages) to scan for malware, and publishes the results. We also consult [OSSF's blocklist](https://github.com/ossf/malicious-packages) and [OSV](osv.dev), as well as other public reports. Updates take effect without any user action.
- **Holds back new versions.** New packages are blocked by default, since malware is usually detected within a day. (Although this can be disabled, configured, and bypassed).
- **Alerts you.** On macOS and Windows a popup names the blocked package and links its advisory.
- **Two modes:**
- **Machine-wide:** screens downloads from every app, across your entire machine.
- **Command prefix:** e.g., `firewall npm install left-pad`, or create an alias with `alias npm='firewall npm`.
## Configuration
Settings are in a `config.json`:
- Machine-wide: `/Library/Application Support/Firewall/` on macOS, `%ProgramData%\Strix Firewall\`
on Windows. Only an admin can edit it; restart from the menu bar or tray to apply.
- Command prefix: `~/.config/firewall/` on Linux, `~/Library/Application Support/com.usestrix.firewall/`
on macOS, `%APPDATA%\usestrix\firewall\config\` on Windows.
```json
{
"min_age_hours": 48,
"allow": { "npm": [{ "name": "left-pad", "version": "1.3.0" }] },
"registries": {
"npm": ["https://npm.example.com/"],
"pypi": ["https://pypi.example.com/"]
},
"telemetry": false
}
```
| Key | Default | |
|---|---|---|
| `min_age_hours` | `24` | How long new versions are held back. `0` turns this off. |
| `allow` | none | Packages exempt from the hold, by ecosystem (`npm`, `pypi`, `cargo`, `gem`, `go`). Omit `version` for every version. Known malware is still blocked. |
| `registries` | none | Mirrors, by the registry they copy (`npm`, `pypi`, `gem`, `go`, `cargo/index`). Both modes screen every mirror listed; command-prefix mode also downloads from the first one. |
| `fail_open` | `true` | Allow a package when it can't be checked. |
| `telemetry` | `true` | Log anonymized usage events to Strix. |
| `port` | `8080` | Local port for command-prefix mode. |
## License
Apache 2.0. See [LICENSE](LICENSE).
+1 -1
View File
@@ -3393,7 +3393,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "strix-firewall"
version = "0.1.2"
version = "0.1.0-rc.1"
dependencies = [
"clap",
"core-foundation 0.10.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "strix-firewall"
version = "0.1.2"
version = "0.1.0-rc.1"
edition = "2021"
license = "Apache-2.0"
authors = ["Strix"]
+18 -24
View File
@@ -11,12 +11,9 @@ pub static PYPI: Registry = Registry {
normalize: Some(pep503),
patterns: &[
Pattern { re: r"^simple/(?<name>[^/]+)", rewrite: Some(FILES) },
// A mirror serves the files from its index host.
WHEEL,
SDIST,
Pattern { re: "", rewrite: Some(FILES) },
],
published: PUBLISHED,
published: None,
};
/// The directories are hash shards, but the filename names the distribution.
@@ -27,28 +24,25 @@ pub static PYPI_FILES: Registry = Registry {
upstream: "https://files.pythonhosted.org/",
index: false,
normalize: Some(pep503),
patterns: &[WHEEL, SDIST],
published: PUBLISHED,
patterns: &[
// A wheel escapes - _ and . to _, so the first two fields are exactly name and version.
Pattern {
re: r"^packages/.+/(?<name>[^-/]+)-(?<version>[^-/]+)-[^/]+\.whl(?:\.metadata)?$",
rewrite: None,
},
// An sdist keeps the name's hyphens, so the version is the last field starting with a digit.
Pattern {
re: r"^packages/.+/(?<name>.+)-(?<version>\d[^-/]*)\.(?:tar\.gz|zip|tar\.bz2)$",
rewrite: None,
},
],
published: Some(Published {
url: "https://pypi.org/pypi/{name}/{version}/json",
pointer: "/urls/0/upload_time_iso_8601",
escape: None,
}),
};
// A wheel escapes - _ and . to _, so the first two fields are exactly name and version.
const WHEEL: Pattern = Pattern {
re: r"^packages/.+/(?<name>[^-/]+)-(?<version>[^-/]+)-[^/]+\.whl(?:\.metadata)?$",
rewrite: None,
};
// An sdist keeps the name's hyphens, so the version is the last field starting with a digit.
const SDIST: Pattern = Pattern {
re: r"^packages/.+/(?<name>.+)-(?<version>\d[^-/]*)\.(?:tar\.gz|zip|tar\.bz2)$",
rewrite: None,
};
const PUBLISHED: Option<Published> = Some(Published {
url: "https://pypi.org/pypi/{name}/{version}/json",
pointer: "/urls/0/upload_time_iso_8601",
escape: None,
});
/// PEP 503: runs of - _ and . collapse to one -, lowercased, which also un-escapes a wheel.
fn pep503(name: &str) -> String {
let mut out = String::with_capacity(name.len());
-9
View File
@@ -84,15 +84,6 @@ async fn a_mirror_is_screened_like_the_registry_it_copies() {
assert!(loaded.is_err());
}
#[tokio::test]
async fn a_pypi_mirror_screens_the_files_it_serves() {
let config = r#"{"registries": {"pypi": ["https://art.example.com/pypi/"]}}"#;
let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
let url = "https://art.example.com/pypi/packages/ab/cd/six-1.16.0-py2.py3-none-any.whl";
let (registry, rest) = firewall.match_on(url, |r| r.upstream).expect("the mirror");
assert_eq!(firewall.resolve(registry, rest).pkg.map(|p| p.label()), Some("[email protected]".into()));
}
#[tokio::test]
async fn an_allowed_version_is_still_checked_for_malware() {
// The unreachable denylist shows that it still runs.
-6
View File
@@ -8,12 +8,6 @@ installers = ["shell", "powershell", "msi"]
# core/wix/main.wxs is edited to install the service.
allow-dirty = ["msi"]
features = ["machine-wide"]
source-tarball = false
publish-jobs = ["./publish-crates"]
post-announce-jobs = ["./release-notes"]
global-artifacts-jobs = ["./build-macos-pkg"]
github-build-setup = "../build-setup.yml"
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
[dist.github-custom-job-permissions]
"publish-crates" = { contents = "read" }
+4 -3
View File
@@ -33,11 +33,12 @@ enum SetupWindow {
private struct MenuBarLabel: View {
@ObservedObject var model: FirewallModel
@Environment(\.openWindow) private var openWindow
@State private var raised = false
var body: some View {
Image(nsImage: Self.icon).task {
await model.refresh()
guard !model.configured else { return }
Image(nsImage: Self.icon).onAppear {
guard !raised, !model.screening else { return }
raised = true
SetupWindow.open(openWindow)
}
}
+4 -14
View File
@@ -2,7 +2,6 @@ import AppKit
import NetworkExtension
import OSLog
import Security
import ServiceManagement
import SystemExtensions
enum StepStatus: Equatable {
@@ -72,8 +71,9 @@ final class FirewallModel: NSObject, ObservableObject {
let running = connection.status == .connected
Task { @MainActor in self?.screening = running }
}
if CommandLine.arguments.contains("--install") {
Task { await install() }
Task {
await refresh()
if CommandLine.arguments.contains("--install") { await install() }
}
Task { await watchBlocks() }
}
@@ -83,10 +83,6 @@ final class FirewallModel: NSObject, ObservableObject {
func refresh() async {
if let manager = try? await loadManager() {
watched = manager
// Nothing starts the tunnel at boot, so the app does once it is set up and not paused.
if manager.isEnabled && manager.connection.status == .disconnected {
try? manager.connection.startVPNTunnel()
}
if manager.isEnabled {
screeningStep = .done("Enabled (\(manager.connection.status.label))")
}
@@ -120,11 +116,6 @@ final class FirewallModel: NSObject, ObservableObject {
work: trustCertificateAuthority)
else { return }
do {
try SMAppService.mainApp.register()
} catch {
log("Could not open the app at login: \(error.localizedDescription)")
}
completed = true
log("Setup complete")
offerRestart()
@@ -165,7 +156,6 @@ final class FirewallModel: NSObject, ObservableObject {
await removeConfiguration()
let strandedKey = removeStoredCA()
await deactivate()
try? await SMAppService.mainApp.unregister()
if let strandedKey { trustStep = .failed(strandedKey) }
let failed = [extensionStep, screeningStep, trustStep].contains(where: \.isFailed)
@@ -240,7 +230,7 @@ final class FirewallModel: NSObject, ObservableObject {
blocked.append(package)
if let allowance = BlockAlert.show(package) { await allow(package, allowance) }
}
if blocked.count > 10 { blocked.removeFirst(blocked.count - 10) }
blocked = Array(blocked.suffix(10))
}
try? await Task.sleep(nanoseconds: 500_000_000)
}