mirror of
https://github.com/anotherhadi/usbguard-tui.git
synced 2026-08-21 03:55:50 +02:00
bbab824a1d
Signed-off-by: Hadi <hadi@example.fr>
232 lines
5.3 KiB
Go
232 lines
5.3 KiB
Go
package guard
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func Check() error {
|
|
_, err := exec.LookPath("usbguard")
|
|
if err != nil {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ListDevices() ([]Device, error) {
|
|
out, err := exec.Command("usbguard", "list-devices").Output()
|
|
if err != nil {
|
|
return nil, wrapExecError(err)
|
|
}
|
|
|
|
deviceRuleText := devicePolicyRuleText()
|
|
|
|
permanentTexts, havePermanentTexts := permanentRuleTexts()
|
|
|
|
implicitTarget, haveImplicitTarget := implicitPolicyTarget()
|
|
|
|
var devices []Device
|
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
d, err := parseLine(line)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
d.RuleState = resolveRuleState(d, deviceRuleText, permanentTexts, havePermanentTexts, implicitTarget, haveImplicitTarget)
|
|
devices = append(devices, d)
|
|
}
|
|
return devices, nil
|
|
}
|
|
|
|
func resolveRuleState(d Device, deviceRuleText map[int]string, permanentTexts map[string]bool, havePermanentTexts bool, implicitTarget Status, haveImplicitTarget bool) RuleState {
|
|
text, matched := deviceRuleText[d.ID]
|
|
if matched {
|
|
if !havePermanentTexts {
|
|
|
|
return RulePermanent
|
|
}
|
|
if permanentTexts[text] {
|
|
return RulePermanent
|
|
}
|
|
return RuleTemporary
|
|
}
|
|
|
|
if !haveImplicitTarget || d.Status != implicitTarget {
|
|
return RuleTemporary
|
|
}
|
|
return RuleDefault
|
|
}
|
|
|
|
func implicitPolicyTarget() (Status, bool) {
|
|
out, err := exec.Command("usbguard", "get-parameter", "ImplicitPolicyTarget").Output()
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return Status(strings.TrimSpace(string(out))), true
|
|
}
|
|
|
|
func DefaultPolicy() Status {
|
|
target, _ := implicitPolicyTarget()
|
|
return target
|
|
}
|
|
|
|
func devicePolicyRuleText() map[int]string {
|
|
out, err := exec.Command("usbguard", "list-rules", "-d").Output()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
result := make(map[int]string)
|
|
var currentRuleText string
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if line[0] == ' ' || line[0] == '\t' {
|
|
trimmed := strings.TrimLeft(line, " \t")
|
|
colonIdx := strings.Index(trimmed, ":")
|
|
if colonIdx < 0 {
|
|
continue
|
|
}
|
|
devID, err := strconv.Atoi(strings.TrimSpace(trimmed[:colonIdx]))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
result[devID] = currentRuleText
|
|
continue
|
|
}
|
|
colonIdx := strings.Index(line, ":")
|
|
if colonIdx < 0 {
|
|
continue
|
|
}
|
|
currentRuleText = normalizeRuleText(line[colonIdx+1:])
|
|
}
|
|
return result
|
|
}
|
|
|
|
func permanentRuleTexts() (map[string]bool, bool) {
|
|
path := ruleFilePath()
|
|
if path == "" {
|
|
return nil, false
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
texts := make(map[string]bool)
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
texts[normalizeRuleText(line)] = true
|
|
}
|
|
return texts, true
|
|
}
|
|
|
|
func normalizeRuleText(s string) string {
|
|
return strings.Join(strings.Fields(s), " ")
|
|
}
|
|
|
|
func AllowDevice(id int, permanent bool) error { return applyPolicy("allow-device", id, permanent) }
|
|
func BlockDevice(id int, permanent bool) error { return applyPolicy("block-device", id, permanent) }
|
|
func RejectDevice(id int, permanent bool) error { return applyPolicy("reject-device", id, permanent) }
|
|
|
|
func DaemonStatus() string {
|
|
out, err := exec.Command("systemctl", "is-active", "usbguard").Output()
|
|
if err != nil {
|
|
return "unknown"
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func applyPolicy(cmd string, id int, permanent bool) error {
|
|
args := []string{cmd}
|
|
if permanent {
|
|
args = append(args, "-p")
|
|
}
|
|
args = append(args, strconv.Itoa(id))
|
|
out, err := exec.Command("usbguard", args...).CombinedOutput()
|
|
if err != nil {
|
|
return classifyError(string(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func wrapExecError(err error) error {
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
return classifyError(string(exitErr.Stderr))
|
|
}
|
|
return err
|
|
}
|
|
|
|
func IsRulesManaged() bool {
|
|
return strings.HasPrefix(ruleFilePath(), "/nix/store/")
|
|
}
|
|
|
|
func RulesWritable() (bool, bool) {
|
|
path := ruleFilePath()
|
|
if path == "" {
|
|
return false, false
|
|
}
|
|
f, err := os.OpenFile(path, os.O_WRONLY, 0)
|
|
if err != nil {
|
|
return false, true
|
|
}
|
|
f.Close()
|
|
return true, true
|
|
}
|
|
|
|
func ruleFilePath() string {
|
|
out, err := exec.Command("systemctl", "cat", "usbguard").Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
configPath := extractConfigPath(string(out))
|
|
if configPath == "" {
|
|
return ""
|
|
}
|
|
return parseRuleFilePath(configPath)
|
|
}
|
|
|
|
func extractConfigPath(s string) string {
|
|
fields := strings.Fields(s)
|
|
for i, f := range fields {
|
|
if f == "-c" && i+1 < len(fields) {
|
|
return fields[i+1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseRuleFilePath(configPath string) string {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if after, ok := strings.CutPrefix(line, "RuleFile="); ok {
|
|
return strings.TrimSpace(after)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func classifyError(output string) error {
|
|
lower := strings.ToLower(output)
|
|
switch {
|
|
case strings.Contains(lower, "permission denied"), strings.Contains(lower, "not authorized"),
|
|
strings.Contains(lower, "operation not permitted"):
|
|
return ErrPermission
|
|
case strings.Contains(lower, "read-only"), strings.Contains(lower, "immutable"):
|
|
return ErrReadOnly
|
|
default:
|
|
return errors.New(strings.TrimSpace(output))
|
|
}
|
|
}
|