Compare commits

...
3 changed files with 40 additions and 20 deletions

No files matched your search

+25 -5
View File
@@ -2,13 +2,13 @@ import AppKit
import Security
struct BlockedPackage: Codable, Hashable, Identifiable {
let id: UInt64
let eco: String
let name: String
let version: String?
let filter: String
let reason: String
var id: String { "\(eco)/\(name)@\(version ?? "")" }
var label: String { version.map { "\(name) \($0)" } ?? name }
/// The denylist and OSV only flag packages known to be malicious; the age check only holds back new ones.
var malicious: Bool { filter != "age" }
@@ -16,10 +16,15 @@ struct BlockedPackage: Codable, Hashable, Identifiable {
var trustable: Bool { !malicious && version != nil }
}
enum Allowance {
/// For a minute, so the rest of that install can fetch it too.
case once
case always
}
@MainActor
enum BlockAlert {
/// Returns whether the user chose to trust the package.
static func show(_ package: BlockedPackage) -> Bool {
static func show(_ package: BlockedPackage) -> Allowance? {
let alert = NSAlert()
alert.addButton(withTitle: "OK")
if package.malicious {
@@ -31,10 +36,25 @@ enum BlockAlert {
} else {
alert.messageText = "Held back a new package"
alert.informativeText = "\(package.label) (\(package.eco)) \(package.reason)."
if package.trustable { alert.addButton(withTitle: "Trust this version") }
if package.trustable {
alert.addButton(withTitle: "Allow Once")
alert.addButton(withTitle: "Trust this version")
}
}
NSApp.activate(ignoringOtherApps: true)
return alert.runModal() == .alertSecondButtonReturn
switch alert.runModal() {
case .alertSecondButtonReturn: return .once
case .alertThirdButtonReturn: return .always
default: return nil
}
}
static func late(_ package: BlockedPackage, _ allowance: Allowance) {
let alert = NSAlert()
alert.messageText = allowance == .once ? "Allowed \(package.label) for the next minute" : "Trusted \(package.label)"
alert.informativeText = "The install had already stopped waiting for it. Run it again to finish."
NSApp.activate(ignoringOtherApps: true)
alert.runModal()
}
}
+10 -14
View File
@@ -203,26 +203,22 @@ final class FirewallModel: NSObject, ObservableObject {
if let url = URL(string: pane) { NSWorkspace.shared.open(url) }
}
/// Allowing a version writes it into the extension's config, which a restart then loads.
func trust(_ package: BlockedPackage) async {
/// Trusting changes the config for every user, so it takes an admin.
func allow(_ package: BlockedPackage, _ allowance: Allowance) async {
guard package.trustable else { return }
guard AdminAuthorization.confirm() else {
if allowance == .always, !AdminAuthorization.confirm() {
log("Trust cancelled for \(package.label)")
return
}
do {
var request = ["eco": package.eco, "name": package.name]
request["version"] = package.version
let json = String(decoding: try JSONSerialization.data(withJSONObject: request), as: UTF8.self)
let reply = try await providerMessage("allow \(json)")
guard reply == "ok" else { throw FirewallError.extensionRefused(reply) }
let reply = try await providerMessage("\(allowance == .once ? "allow-once" : "allow") \(package.id)")
guard reply == "ok" || reply == "late" else { throw FirewallError.extensionRefused(reply) }
blocked.removeAll { $0 == package }
log("Trusted \(package.label) (\(package.eco))")
log("Allowed \(package.label) (\(package.eco)) \(allowance)")
if reply == "late" { BlockAlert.late(package, allowance) }
} catch {
log("Could not trust \(package.label): \(error.localizedDescription)")
return
log("Could not allow \(package.label): \(error.localizedDescription)")
}
await restart()
}
/// The extension cannot message the app, so the app asks it for blocks.
@@ -232,11 +228,11 @@ final class FirewallModel: NSObject, ObservableObject {
let packages = try? JSONDecoder().decode([BlockedPackage].self, from: Data(reply.utf8)) {
for package in packages where !blocked.contains(package) {
blocked.append(package)
if BlockAlert.show(package) { await trust(package) }
if let allowance = BlockAlert.show(package) { await allow(package, allowance) }
}
blocked = Array(blocked.suffix(10))
}
try? await Task.sleep(nanoseconds: 2_000_000_000)
try? await Task.sleep(nanoseconds: 500_000_000)
}
}
+5 -1
View File
@@ -29,8 +29,12 @@ struct MenuBarView: View {
if !package.malicious {
Menu("Held back \(package.label) (\(package.eco))") {
Text(package.reason)
Button("Allow Once") {
Task { await model.allow(package, .once) }
}
.disabled(model.busy || !package.trustable)
Button("Trust this version") {
Task { await model.trust(package) }
Task { await model.allow(package, .always) }
}
.disabled(model.busy || !package.trustable)
}