mirror of
https://github.com/anotherhadi/usbguard-tui.git
synced 2026-05-11 22:02:34 +02:00
Init
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
Reference in New Issue
Block a user