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
1 change: 1 addition & 0 deletions Flowlight/Localization/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/pt-PT.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@
"Modify responses" = "待翻译:Modify responses";
"Add Guardrail" = "待翻译:Add Guardrail";
"Save this as a normal Flowlight guardrail" = "待翻译:Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "待翻译:Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "待翻译:Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "待翻译:Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "待翻译:%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
1 change: 1 addition & 0 deletions Flowlight/Localization/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@
"Modify responses" = "Modify responses";
"Add Guardrail" = "Add Guardrail";
"Save this as a normal Flowlight guardrail" = "Save this as a normal Flowlight guardrail";
"Save this as a normal Flowlight rule" = "Save this as a normal Flowlight rule";
"Plugin findings are advisory. They did not block or change this request." = "Plugin findings are advisory. They did not block or change this request.";
"Let the request reach %@, then change its eligible response before this app receives it" = "Let the request reach %@, then change its eligible response before this app receives it";
"%@ answered this request. Flowlight then changed the response with “%@” before the app received it." = "%@ answered this request. Flowlight then changed the response with “%@” before the app received it.";
Expand Down
5 changes: 3 additions & 2 deletions Flowlight/Plugins/PluginEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ enum PluginEngine {

private extension InspectionPluginRule {
func finding(_ severity: PluginFinding.Severity, _ title: String, _ summary: String,
_ evidence: [PluginEvidence], suggestedGuardrail: Guardrail? = nil) -> PluginFindingDraft {
_ evidence: [PluginEvidence], suggestedGuardrail: Guardrail? = nil, suggestedRule: Rule? = nil) -> PluginFindingDraft {
PluginFindingDraft(pluginID: manifest.id, pluginVersion: manifest.version, kind: manifest.kind, severity: severity,
title: title, summary: summary, evidence: evidence, suggestedGuardrail: suggestedGuardrail)
title: title, summary: summary, evidence: evidence,
suggestedGuardrail: suggestedGuardrail, suggestedRule: suggestedRule)
}
}

Expand Down
4 changes: 3 additions & 1 deletion Flowlight/Plugins/PluginModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ struct PluginFinding: Codable, Identifiable, Equatable, Sendable {
var summary: String
var evidence: [PluginEvidence]
var suggestedGuardrail: Guardrail?
var suggestedRule: Rule?
var createdAt: Date

enum Severity: String, Codable, Sendable, CaseIterable { case info, low, medium, high }
Expand All @@ -301,12 +302,13 @@ struct PluginFindingDraft: Equatable, Sendable {
var summary: String
var evidence: [PluginEvidence]
var suggestedGuardrail: Guardrail?
var suggestedRule: Rule?

func materialize(exchangeID: Int64, createdAt: Date = Date()) -> PluginFinding {
let stable = [String(exchangeID), pluginID, title].joined(separator: "|")
return PluginFinding(id: StablePluginID.make(stable), exchangeID: exchangeID, pluginID: pluginID, pluginVersion: pluginVersion,
kind: kind, severity: severity, title: title, summary: summary, evidence: evidence,
suggestedGuardrail: suggestedGuardrail, createdAt: createdAt)
suggestedGuardrail: suggestedGuardrail, suggestedRule: suggestedRule, createdAt: createdAt)
}
}

Expand Down
36 changes: 29 additions & 7 deletions Flowlight/Plugins/PluginScriptRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,44 @@ final class PluginScriptRunner: @unchecked Sendable {
!label.isEmpty, !value.isEmpty else { return nil }
return PluginEvidence(label, value)
}
let guardrail = suggestedGuardrail(from: object["suggestedGuardrail"], exchange: exchange)
let maySuggest = manifest.capabilities.contains(.suggestGuardrail) && manifest.permissions.contains(.suggestGuardrail)
let guardrail = maySuggest ? suggestedGuardrail(from: object["suggestedGuardrail"], exchange: exchange) : nil
let rule = maySuggest ? suggestedRule(from: object["suggestedRule"], exchange: exchange) : nil
return PluginFindingDraft(pluginID: manifest.id, pluginVersion: manifest.version, kind: manifest.kind, severity: severity,
title: title, summary: summary, evidence: evidence, suggestedGuardrail: guardrail)
title: title, summary: summary, evidence: evidence,
suggestedGuardrail: guardrail, suggestedRule: rule)
}

private func suggestedGuardrail(from value: Any?, exchange: HTTPExchange) -> Guardrail? {
guard let object = value as? [String: Any] else { return nil }
var guardrail = Guardrail(agent: object["agent"] as? String ?? exchange.agent ?? "",
server: object["server"] as? String ?? "",
tool: object["tool"] as? String ?? "",
var guardrail = Guardrail(agent: limited(object["agent"], fallback: exchange.agent ?? "", count: 120),
server: limited(object["server"], count: 120),
tool: limited(object["tool"], count: 120),
origin: .observed)
guardrail.resource = object["resource"] as? String ?? ""
guardrail.name = String((object["name"] as? String ?? "").prefix(120))
guardrail.resource = limited(object["resource"], count: 240)
guardrail.name = limited(object["name"], count: 120)
return guardrail.isComplete ? guardrail : nil
}

private func suggestedRule(from value: Any?, exchange: HTTPExchange) -> Rule? {
guard let object = value as? [String: Any] else { return nil }
let action = Rule.Action(rawValue: limited(object["action"], fallback: "block", count: 20)) ?? .block
let destination = limited(object["destination"], fallback: exchange.host, count: 240)
var rule = Rule(action: action,
app: limited(object["app"], fallback: exchange.agent ?? exchange.bundleID, count: 160),
destination: AgentPolicy.normalize(destination) ?? destination.lowercased(),
path: limited(object["path"], count: 240),
method: limited(object["method"], count: 20).uppercased(),
origin: .alert)
if let status = object["status"] as? Int { rule.status = min(max(status, 400), 599) }
rule.name = limited(object["name"], count: 120)
return rule.isComplete ? rule : nil
}

private func limited(_ value: Any?, fallback: String = "", count: Int = 120) -> String {
String(((value as? String) ?? fallback).trimmingCharacters(in: .whitespacesAndNewlines).prefix(count))
}

private func manifestPayload(_ manifest: PluginManifest) -> [String: Any] {
[
"id": manifest.id,
Expand Down
25 changes: 19 additions & 6 deletions Flowlight/Storage/TrafficDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ final class TrafficDatabase: @unchecked Sendable {
CREATE INDEX IF NOT EXISTS plugin_findings_exchange ON plugin_findings (exchange_id);
CREATE INDEX IF NOT EXISTS plugin_findings_plugin_created ON plugin_findings (plugin_id, created_at);
""")
let pluginFindingColumns = Set(try conn.query("PRAGMA table_info(plugin_findings)") { $0.text(1) })
if !pluginFindingColumns.contains("suggested_rule") {
try conn.execute("ALTER TABLE plugin_findings ADD COLUMN suggested_rule TEXT NOT NULL DEFAULT ''")
}
// Added with blocking: the destination a refused connection was headed for, so its alert can offer to
// allow it without parsing the sentence back out of `detail`.
let alertColumns = Set(try conn.query("PRAGMA table_info(alerts)") { $0.text(1) })
Expand Down Expand Up @@ -1073,12 +1077,18 @@ final class TrafficDatabase: @unchecked Sendable {
} else {
guardrail = ""
}
let rule: String
if let suggested = finding.suggestedRule {
rule = String(decoding: try encoder.encode(suggested), as: UTF8.self)
} else {
rule = ""
}
try conn.run("""
INSERT OR IGNORE INTO plugin_findings (id, exchange_id, plugin_id, plugin_version, kind, severity, title, summary, evidence, suggested_guardrail, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
INSERT OR IGNORE INTO plugin_findings (id, exchange_id, plugin_id, plugin_version, kind, severity, title, summary, evidence, suggested_guardrail, suggested_rule, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
""", [.text(finding.id), .int(exchangeID), .text(finding.pluginID), .text(finding.pluginVersion), .text(finding.kind.rawValue),
.text(finding.severity.rawValue), .text(finding.title), .text(String(finding.summary.prefix(500))), .text(evidence),
.text(guardrail), .double(finding.createdAt.timeIntervalSince1970)])
.text(guardrail), .text(rule), .double(finding.createdAt.timeIntervalSince1970)])
}
}

Expand All @@ -1089,7 +1099,7 @@ final class TrafficDatabase: @unchecked Sendable {
let marks = Array(repeating: "?", count: exchangeIDs.count).joined(separator: ",")
let values = exchangeIDs.map(SQLValue.int)
let rows = try conn.query("""
SELECT id, exchange_id, plugin_id, plugin_version, kind, severity, title, summary, evidence, suggested_guardrail, created_at
SELECT id, exchange_id, plugin_id, plugin_version, kind, severity, title, summary, evidence, suggested_guardrail, suggested_rule, created_at
FROM plugin_findings WHERE exchange_id IN (\(marks)) ORDER BY created_at ASC, title ASC
""", values) { row in decodePluginFinding(row) }
let visible = visibleOnly ? rows.filter { enabled[$0.pluginID] ?? true } : rows
Expand All @@ -1105,11 +1115,14 @@ final class TrafficDatabase: @unchecked Sendable {
let evidence = (try? decoder.decode([PluginEvidence].self, from: Data(row.text(8).utf8))) ?? []
let guardrailText = row.text(9)
let guardrail = guardrailText.isEmpty ? nil : try? decoder.decode(Guardrail.self, from: Data(guardrailText.utf8))
let ruleText = row.text(10)
let rule = ruleText.isEmpty ? nil : try? decoder.decode(Rule.self, from: Data(ruleText.utf8))
return PluginFinding(id: row.text(0), exchangeID: row.int(1), pluginID: row.text(2), pluginVersion: row.text(3),
kind: PluginManifest.Kind(rawValue: row.text(4)) ?? .traffic,
severity: PluginFinding.Severity(rawValue: row.text(5)) ?? .info,
title: row.text(6), summary: row.text(7), evidence: evidence, suggestedGuardrail: guardrail,
createdAt: Date(timeIntervalSince1970: row.double(10)))
title: row.text(6), summary: row.text(7), evidence: evidence,
suggestedGuardrail: guardrail, suggestedRule: rule,
createdAt: Date(timeIntervalSince1970: row.double(11)))
}

// MARK: Rule simulation
Expand Down
Loading
Loading