Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-04-30 17:33:42 +02:00
commit 09b054cc5c
16 changed files with 1037 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
package guard
import (
"errors"
"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)
}
var devices []Device
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line == "" {
continue
}
d, err := parseLine(line)
if err == nil {
devices = append(devices, d)
}
}
return devices, nil
}
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 classifyError(output string) error {
lower := strings.ToLower(output)
switch {
case strings.Contains(lower, "permission denied"), strings.Contains(lower, "not authorized"):
return ErrPermission
case strings.Contains(lower, "read-only"), strings.Contains(lower, "immutable"):
return ErrReadOnly
default:
return errors.New(strings.TrimSpace(output))
}
}
+88
View File
@@ -0,0 +1,88 @@
package guard
import (
"errors"
"fmt"
"strconv"
"strings"
)
type Status string
const (
Allowed Status = "allow"
Blocked Status = "block"
Rejected Status = "reject"
)
type Device struct {
ID int
Name string
Status Status
VidPid string
}
func (d Device) Title() string { return d.Name }
func (d Device) Description() string { return fmt.Sprintf("id:%-3d %s", d.ID, d.VidPid) }
func (d Device) FilterValue() string { return d.Name + " " + d.VidPid }
// parseLine parses a line from "usbguard list-devices":
// 1: allow id 04b3:301b serial "" name "USB Hub" hash "..." via-port "usb1"
func parseLine(line string) (Device, error) {
colonIdx := strings.Index(line, ":")
if colonIdx < 0 {
return Device{}, errors.New("invalid format")
}
id, err := strconv.Atoi(strings.TrimSpace(line[:colonIdx]))
if err != nil {
return Device{}, err
}
rest := strings.TrimSpace(line[colonIdx+1:])
parts := strings.Fields(rest)
if len(parts) < 1 {
return Device{}, errors.New("missing status")
}
status := Status(parts[0])
name := extractField(rest, "name")
if name == "" {
name = fmt.Sprintf("Unknown Device #%d", id)
}
return Device{
ID: id,
Name: name,
Status: status,
VidPid: extractUnquoted(rest, "id"),
}, nil
}
func extractField(rule, field string) string {
prefix := field + ` "`
idx := strings.Index(rule, prefix)
if idx < 0 {
return ""
}
rest := rule[idx+len(prefix):]
end := strings.Index(rest, `"`)
if end < 0 {
return ""
}
return rest[:end]
}
func extractUnquoted(rule, field string) string {
prefix := field + " "
idx := strings.Index(rule, prefix)
if idx < 0 {
return ""
}
rest := rule[idx+len(prefix):]
end := strings.IndexAny(rest, " \t\n")
if end < 0 {
return rest
}
return rest[:end]
}
+9
View File
@@ -0,0 +1,9 @@
package guard
import "errors"
var (
ErrNotFound = errors.New("usbguard not found in PATH")
ErrPermission = errors.New("insufficient permissions to manage devices")
ErrReadOnly = errors.New("rules file is read-only")
)