Compare commits
8 Commits
dfa3217037
...
79957f8ea7
Author | SHA1 | Date | |
---|---|---|---|
79957f8ea7 | |||
7e52463445 | |||
89970f5197 | |||
35037705a9 | |||
647c6ea21b | |||
416d93e880 | |||
312753924b | |||
54308f79d2 |
@ -7,8 +7,10 @@ import (
|
||||
)
|
||||
|
||||
// New initialises a root Node.
|
||||
func New(output io.Writer, logf LogFunc, name string) Command {
|
||||
return rootNode{newNode(output, logf, name, "")}
|
||||
func New(output io.Writer, logf LogFunc, name string, early HandlerFunc) Command {
|
||||
c := rootNode{newNode(output, logf, name, "")}
|
||||
c.f = early
|
||||
return c
|
||||
}
|
||||
|
||||
func newNode(output io.Writer, logf LogFunc, name, usage string) *node {
|
||||
@ -31,6 +33,11 @@ func newNode(output io.Writer, logf LogFunc, name, usage string) *node {
|
||||
}
|
||||
|
||||
func (n *node) Command(name, usage string, f HandlerFunc) Node {
|
||||
n.NewCommand(name, usage, f)
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) NewCommand(name, usage string, f HandlerFunc) Flag[Node] {
|
||||
if f == nil {
|
||||
panic("invalid handler")
|
||||
}
|
||||
@ -43,7 +50,7 @@ func (n *node) Command(name, usage string, f HandlerFunc) Node {
|
||||
if !n.adopt(s) {
|
||||
panic("attempted to initialise subcommand with non-unique name")
|
||||
}
|
||||
return n
|
||||
return s
|
||||
}
|
||||
|
||||
func (n *node) New(name, usage string) Node {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
c := command.New(nil, nil, "test")
|
||||
c := command.New(nil, nil, "test", nil)
|
||||
stubHandler := func([]string) error { panic("unreachable") }
|
||||
|
||||
t.Run("nil direct handler", func(t *testing.T) {
|
||||
|
@ -6,6 +6,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UsageInternal causes the command to be hidden from help text when set as the usage string.
|
||||
const UsageInternal = "internal"
|
||||
|
||||
type (
|
||||
// HandlerFunc is called when matching a directly handled subcommand tree.
|
||||
HandlerFunc = func(args []string) error
|
||||
@ -19,6 +22,11 @@ type (
|
||||
Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string)
|
||||
}
|
||||
|
||||
Flag[T any] interface {
|
||||
// Flag defines a generic flag type in Node's flag set.
|
||||
Flag(p any, name string, value FlagDefiner, usage string) T
|
||||
}
|
||||
|
||||
Command interface {
|
||||
Parse(arguments []string) error
|
||||
baseNode[Command]
|
||||
@ -28,10 +36,15 @@ type (
|
||||
baseNode[T any] interface {
|
||||
// Command appends a subcommand with direct command handling.
|
||||
Command(name, usage string, f HandlerFunc) T
|
||||
// Flag defines a generic flag type in Node's flag set.
|
||||
Flag(p any, name string, value FlagDefiner, usage string) T
|
||||
|
||||
// New returns a new subcommand tree.
|
||||
New(name, usage string) (sub Node)
|
||||
// NewCommand returns a new subcommand with direct command handling.
|
||||
NewCommand(name, usage string, f HandlerFunc) (sub Flag[Node])
|
||||
|
||||
// PrintHelp prints a help message to the configured writer.
|
||||
PrintHelp()
|
||||
|
||||
Flag[T]
|
||||
}
|
||||
)
|
||||
|
@ -28,6 +28,14 @@ func (v StringFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, u
|
||||
b.WriteString(" [" + prettyFlag(name) + " <value>]")
|
||||
}
|
||||
|
||||
// IntFlag is the default value of an int flag.
|
||||
type IntFlag int
|
||||
|
||||
func (v IntFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
|
||||
set.IntVar(p.(*int), name, int(v), usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <int>]")
|
||||
}
|
||||
|
||||
// BoolFlag is the default value of a bool flag.
|
||||
type BoolFlag bool
|
||||
|
||||
@ -36,6 +44,26 @@ func (v BoolFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usa
|
||||
b.WriteString(" [" + prettyFlag(name) + "]")
|
||||
}
|
||||
|
||||
// RepeatableFlag implements an ordered, repeatable string flag.
|
||||
type RepeatableFlag []string
|
||||
|
||||
func (r *RepeatableFlag) String() string {
|
||||
if r == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return strings.Join(*r, " ")
|
||||
}
|
||||
|
||||
func (r *RepeatableFlag) Set(v string) error {
|
||||
*r = append(*r, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepeatableFlag) Define(b *strings.Builder, set *flag.FlagSet, _ any, name, usage string) {
|
||||
set.Var(r, name, usage)
|
||||
b.WriteString(" [" + prettyFlag(name) + " <value>]")
|
||||
}
|
||||
|
||||
// this has no effect on parse outcome
|
||||
func prettyFlag(name string) string {
|
||||
switch len(name) {
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
|
||||
var ErrHelp = errors.New("help requested")
|
||||
|
||||
func (n *node) PrintHelp() { _ = n.writeHelp() }
|
||||
|
||||
func (n *node) writeHelp() error {
|
||||
if _, err := fmt.Fprintf(n.out,
|
||||
"\nUsage:\t%s [-h | --help]%s COMMAND [OPTIONS]\n",
|
||||
@ -42,8 +44,10 @@ func (n *node) writeCommands(w io.Writer) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "\t%s\t%s\n", n.name, n.usage); err != nil {
|
||||
return err
|
||||
if n.usage != UsageInternal {
|
||||
if _, err := fmt.Fprintf(w, "\t%s\t%s\n", n.name, n.usage); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return n.next.writeCommands(w)
|
||||
}
|
||||
|
@ -34,9 +34,6 @@ func (n *node) Parse(arguments []string) error {
|
||||
|
||||
match:
|
||||
if n.child != nil {
|
||||
if n.f != nil {
|
||||
panic("invalid subcommand tree state")
|
||||
}
|
||||
// propagate help prefix early: flag set usage dereferences help
|
||||
n.child.prefix = append(n.prefix, n.name)
|
||||
}
|
||||
@ -50,6 +47,17 @@ match:
|
||||
args := n.set.Args()
|
||||
|
||||
if n.child != nil {
|
||||
if n.f != nil {
|
||||
if n.usage != "" { // root node early special case
|
||||
panic("invalid subcommand tree state")
|
||||
}
|
||||
|
||||
// special case: root node calls HandlerFunc for initialisation
|
||||
if err := n.f(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return n.writeHelp()
|
||||
}
|
||||
|
@ -24,13 +24,13 @@ func TestParse(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"d=0 empty sub",
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root") },
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
|
||||
[]string{""},
|
||||
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
{
|
||||
"d=0 empty sub garbage",
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root") },
|
||||
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
|
||||
},
|
||||
@ -64,18 +64,42 @@ func TestParse(t *testing.T) {
|
||||
[]string{"print", "0", "1", "2"},
|
||||
"012", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 out of order string flag",
|
||||
buildTestCommand,
|
||||
[]string{"string", "--string", "64d3b4b7b21788585845060e2199a78f"},
|
||||
"flag provided but not defined: -string\n\nUsage:\ttest string [-h | --help] COMMAND [OPTIONS]\n\n", "",
|
||||
errors.New("flag provided but not defined: -string"),
|
||||
},
|
||||
{
|
||||
"d=0 string flag",
|
||||
buildTestCommand,
|
||||
[]string{"--val", "64d3b4b7b21788585845060e2199a78f", "flag"},
|
||||
[]string{"--string", "64d3b4b7b21788585845060e2199a78f", "string"},
|
||||
"64d3b4b7b21788585845060e2199a78f", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 out of order string flag",
|
||||
"d=0 int flag",
|
||||
buildTestCommand,
|
||||
[]string{"flag", "--val", "64d3b4b7b21788585845060e2199a78f"},
|
||||
"flag provided but not defined: -val\n\nUsage:\ttest flag [-h | --help] COMMAND [OPTIONS]\n\n", "",
|
||||
errors.New("flag provided but not defined: -val"),
|
||||
[]string{"--int", "2147483647", "int"},
|
||||
"2147483647", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 repeat flag",
|
||||
buildTestCommand,
|
||||
[]string{"--repeat", "0", "--repeat", "1", "--repeat", "2", "--repeat", "3", "--repeat", "4", "repeat"},
|
||||
"[0 1 2 3 4]", "", nil,
|
||||
},
|
||||
{
|
||||
"d=0 bool flag",
|
||||
buildTestCommand,
|
||||
[]string{"-v", "succeed"},
|
||||
"", "test: verbose\n", nil,
|
||||
},
|
||||
{
|
||||
"d=0 bool flag early error",
|
||||
buildTestCommand,
|
||||
[]string{"--fail", "succeed"},
|
||||
"", "", errSuccess,
|
||||
},
|
||||
|
||||
{
|
||||
@ -126,12 +150,14 @@ func TestParse(t *testing.T) {
|
||||
buildTestCommand,
|
||||
[]string{},
|
||||
`
|
||||
Usage: test [-h | --help] [-v] [--val <value>] COMMAND [OPTIONS]
|
||||
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
error return an error
|
||||
print wraps Fprint
|
||||
flag print value passed by flag
|
||||
string print string passed by flag
|
||||
int print int passed by flag
|
||||
repeat print repeated values passed by flag
|
||||
empty empty subcommand
|
||||
join wraps strings.Join
|
||||
succeed this command succeeds
|
||||
@ -144,21 +170,29 @@ Commands:
|
||||
buildTestCommand,
|
||||
[]string{"-h"},
|
||||
`
|
||||
Usage: test [-h | --help] [-v] [--val <value>] COMMAND [OPTIONS]
|
||||
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
error return an error
|
||||
print wraps Fprint
|
||||
flag print value passed by flag
|
||||
string print string passed by flag
|
||||
int print int passed by flag
|
||||
repeat print repeated values passed by flag
|
||||
empty empty subcommand
|
||||
join wraps strings.Join
|
||||
succeed this command succeeds
|
||||
deep top level of command tree with various levels
|
||||
|
||||
Flags:
|
||||
-v verbosity
|
||||
-val string
|
||||
store val for the "flag" command (default "default")
|
||||
-fail
|
||||
fail early
|
||||
-int int
|
||||
store value for the "int" command (default -1)
|
||||
-repeat value
|
||||
store value for the "repeat" command
|
||||
-string string
|
||||
store value for the "string" command (default "default")
|
||||
-v verbose output
|
||||
|
||||
`, "", flag.ErrHelp,
|
||||
},
|
||||
@ -239,11 +273,27 @@ var (
|
||||
)
|
||||
|
||||
func buildTestCommand(wout, wlog io.Writer) (c command.Command) {
|
||||
var val string
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagFail bool
|
||||
|
||||
flagString string
|
||||
flagInt int
|
||||
flagRepeat command.RepeatableFlag
|
||||
)
|
||||
|
||||
logf := newLogFunc(wlog)
|
||||
c = command.New(wout, logf, "test").
|
||||
Flag(new(bool), "v", command.BoolFlag(false), "verbosity").
|
||||
c = command.New(wout, logf, "test", func([]string) error {
|
||||
if flagVerbose {
|
||||
logf("verbose")
|
||||
}
|
||||
if flagFail {
|
||||
return errSuccess
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "verbose output").
|
||||
Flag(&flagFail, "fail", command.BoolFlag(false), "fail early").
|
||||
Command("error", "return an error", func([]string) error {
|
||||
return errSuccess
|
||||
}).
|
||||
@ -255,13 +305,15 @@ func buildTestCommand(wout, wlog io.Writer) (c command.Command) {
|
||||
_, err := fmt.Fprint(wout, a...)
|
||||
return err
|
||||
}).
|
||||
Flag(&val, "val", command.StringFlag("default"), "store val for the \"flag\" command").
|
||||
Command("flag", "print value passed by flag", func(args []string) error {
|
||||
_, err := fmt.Fprint(wout, val)
|
||||
return err
|
||||
})
|
||||
Flag(&flagString, "string", command.StringFlag("default"), "store value for the \"string\" command").
|
||||
Command("string", "print string passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagString); return err }).
|
||||
Flag(&flagInt, "int", command.IntFlag(-1), "store value for the \"int\" command").
|
||||
Command("int", "print int passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagInt); return err }).
|
||||
Flag(nil, "repeat", &flagRepeat, "store value for the \"repeat\" command").
|
||||
Command("repeat", "print repeated values passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagRepeat); return err })
|
||||
|
||||
c.New("empty", "empty subcommand")
|
||||
c.New("hidden", command.UsageInternal)
|
||||
|
||||
c.New("join", "wraps strings.Join").
|
||||
Command("out", "write result to wout", func(args []string) error {
|
||||
|
@ -24,10 +24,10 @@ func TestParseUnreachable(t *testing.T) {
|
||||
// a node with descendents must not have a direct handler
|
||||
t.Run("sub handle conflict", func(t *testing.T) {
|
||||
defer checkRecover(t, "Parse", "invalid subcommand tree state")
|
||||
n := newNode(panicWriter{}, nil, " ", "")
|
||||
n := newNode(panicWriter{}, nil, " ", " ")
|
||||
n.adopt(newNode(panicWriter{}, nil, " ", " "))
|
||||
n.f = func([]string) error { panic("unreachable") }
|
||||
_ = n.Parse(nil)
|
||||
_ = n.Parse([]string{" "})
|
||||
})
|
||||
|
||||
// this would only happen if a node was matched twice
|
||||
|
423
main.go
423
main.go
@ -3,19 +3,19 @@ package main
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"flag"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/helper/seccomp"
|
||||
@ -30,36 +30,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
errSuccess = errors.New("success")
|
||||
|
||||
//go:embed LICENSE
|
||||
license string
|
||||
)
|
||||
|
||||
func init() {
|
||||
fmsg.Prepare("fortify")
|
||||
|
||||
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
||||
flag.BoolVar(&flagJSON, "json", false, "Format output in JSON when applicable")
|
||||
}
|
||||
func init() { fmsg.Prepare("fortify") }
|
||||
|
||||
var std sys.State = new(sys.Std)
|
||||
|
||||
type gl []string
|
||||
|
||||
func (g *gl) String() string {
|
||||
if g == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return strings.Join(*g, " ")
|
||||
}
|
||||
|
||||
func (g *gl) Set(v string) error {
|
||||
*g = append(*g, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
|
||||
init0.TryArgv0()
|
||||
@ -73,112 +53,56 @@ func main() {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
flag.CommandLine.Usage = func() {
|
||||
fmt.Println()
|
||||
fmt.Println("Usage:\tfortify [-v] [--json] COMMAND [OPTIONS]")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
|
||||
commands := [][2]string{
|
||||
{"app", "Launch app defined by the specified config file"},
|
||||
{"run", "Configure and start a permissive default sandbox"},
|
||||
{"show", "Show the contents of an app configuration"},
|
||||
{"ps", "List active apps and their state"},
|
||||
{"version", "Show fortify version"},
|
||||
{"license", "Show full license text"},
|
||||
{"template", "Produce a config template"},
|
||||
{"help", "Show this help message"},
|
||||
}
|
||||
for _, c := range commands {
|
||||
_, _ = fmt.Fprintf(w, "\t%s\t%s\n", c[0], c[1])
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
fmt.Printf("fortify: cannot write command list: %v\n", err)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
flag.Parse()
|
||||
fmsg.Store(flagVerbose)
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
flag.CommandLine.Usage()
|
||||
err := buildCommand(os.Stderr).Parse(os.Args[1:])
|
||||
if errors.Is(err, errSuccess) || errors.Is(err, command.ErrHelp) {
|
||||
internal.Exit(0)
|
||||
panic("unreachable")
|
||||
}
|
||||
if errors.Is(err, command.ErrNoMatch) || errors.Is(err, command.ErrEmptyTree) {
|
||||
internal.Exit(1)
|
||||
panic("unreachable")
|
||||
}
|
||||
if err == nil {
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "version": // print version string
|
||||
if v, ok := internal.Check(internal.Version); ok {
|
||||
fmt.Println(v)
|
||||
} else {
|
||||
fmt.Println("impure")
|
||||
}
|
||||
var flagError command.FlagError
|
||||
if !errors.As(err, &flagError) {
|
||||
log.Printf("command: %v", err)
|
||||
internal.Exit(1)
|
||||
panic("unreachable")
|
||||
}
|
||||
fmsg.Verbose(flagError.Error())
|
||||
if flagError.Success() {
|
||||
internal.Exit(0)
|
||||
}
|
||||
internal.Exit(1)
|
||||
}
|
||||
|
||||
case "license": // print embedded license
|
||||
fmt.Println(license)
|
||||
internal.Exit(0)
|
||||
func buildCommand(out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "fortify", func([]string) error { fmsg.Store(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
|
||||
|
||||
case "template": // print full template configuration
|
||||
printJSON(os.Stdout, false, fst.Template())
|
||||
internal.Exit(0)
|
||||
|
||||
case "help": // print help message
|
||||
flag.CommandLine.Usage()
|
||||
internal.Exit(0)
|
||||
|
||||
case "ps": // print all state info
|
||||
set := flag.NewFlagSet("ps", flag.ExitOnError)
|
||||
var short bool
|
||||
set.BoolVar(&short, "short", false, "Print instance id")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args[1:])
|
||||
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), short)
|
||||
internal.Exit(0)
|
||||
|
||||
case "show": // pretty-print app info
|
||||
set := flag.NewFlagSet("show", flag.ExitOnError)
|
||||
var short bool
|
||||
set.BoolVar(&short, "short", false, "Omit filesystem information")
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args[1:])
|
||||
|
||||
switch len(set.Args()) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, short)
|
||||
|
||||
case 1: // instance
|
||||
name := set.Args()[0]
|
||||
config, instance := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, short)
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
internal.Exit(0)
|
||||
|
||||
case "app": // launch app from configuration file
|
||||
if len(args) < 2 {
|
||||
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("app requires at least 1 argument")
|
||||
}
|
||||
|
||||
// config extraArgs...
|
||||
config := tryPath(args[1])
|
||||
config.Command = append(config.Command, args[2:]...)
|
||||
config := tryPath(args[0])
|
||||
config.Command = append(config.Command, args[1:]...)
|
||||
|
||||
// invoke app
|
||||
runApp(app.MustNew(std), config)
|
||||
panic("unreachable")
|
||||
})
|
||||
|
||||
case "run": // run app in permissive defaults usage pattern
|
||||
set := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
|
||||
{
|
||||
var (
|
||||
dbusConfigSession string
|
||||
dbusConfigSystem string
|
||||
@ -187,135 +111,188 @@ func main() {
|
||||
|
||||
fid string
|
||||
aid int
|
||||
groups gl
|
||||
groups command.RepeatableFlag
|
||||
homeDir string
|
||||
userName string
|
||||
enablements [system.ELen]bool
|
||||
)
|
||||
|
||||
set.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
|
||||
set.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
|
||||
set.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
|
||||
set.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
|
||||
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||
// initialise config from flags
|
||||
config := &fst.Config{
|
||||
ID: fid,
|
||||
Command: args,
|
||||
}
|
||||
|
||||
set.StringVar(&fid, "id", "", "App ID, leave empty to disable security context app_id")
|
||||
set.IntVar(&aid, "a", 0, "Fortify application ID")
|
||||
set.Var(&groups, "g", "Groups inherited by the app process")
|
||||
set.StringVar(&homeDir, "d", "os", "Application home directory")
|
||||
set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
|
||||
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Allow Wayland connections")
|
||||
set.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
|
||||
set.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
|
||||
set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
|
||||
if aid < 0 || aid > 9999 {
|
||||
log.Fatalf("aid %d out of range", aid)
|
||||
}
|
||||
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args[1:])
|
||||
|
||||
// initialise config from flags
|
||||
config := &fst.Config{
|
||||
ID: fid,
|
||||
Command: set.Args(),
|
||||
}
|
||||
|
||||
if aid < 0 || aid > 9999 {
|
||||
log.Fatalf("aid %d out of range", aid)
|
||||
}
|
||||
|
||||
// resolve home/username from os when flag is unset
|
||||
var (
|
||||
passwd *user.User
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
var us string
|
||||
if uid, err := std.Uid(aid); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot obtain uid from fsu:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
us = strconv.Itoa(uid)
|
||||
}
|
||||
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
fmsg.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
Uid: us,
|
||||
Gid: us,
|
||||
Username: "chronos",
|
||||
Name: "Fortify",
|
||||
HomeDir: "/var/empty",
|
||||
// resolve home/username from os when flag is unset
|
||||
var (
|
||||
passwd *user.User
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
var us string
|
||||
if uid, err := std.Uid(aid); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot obtain uid from fsu:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
us = strconv.Itoa(uid)
|
||||
}
|
||||
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
fmsg.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
Uid: us,
|
||||
Gid: us,
|
||||
Username: "chronos",
|
||||
Name: "Fortify",
|
||||
HomeDir: "/var/empty",
|
||||
}
|
||||
} else {
|
||||
passwd = u
|
||||
}
|
||||
} else {
|
||||
passwd = u
|
||||
}
|
||||
)
|
||||
|
||||
if homeDir == "os" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
homeDir = passwd.HomeDir
|
||||
}
|
||||
)
|
||||
|
||||
if homeDir == "os" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
homeDir = passwd.HomeDir
|
||||
}
|
||||
|
||||
if userName == "chronos" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
userName = passwd.Username
|
||||
}
|
||||
|
||||
config.Confinement.AppID = aid
|
||||
config.Confinement.Groups = groups
|
||||
config.Confinement.Outer = homeDir
|
||||
config.Confinement.Username = userName
|
||||
|
||||
// enablements from flags
|
||||
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
|
||||
if enablements[i] {
|
||||
config.Confinement.Enablements.Set(i)
|
||||
if userName == "chronos" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
userName = passwd.Username
|
||||
}
|
||||
}
|
||||
|
||||
// parse D-Bus config file from flags if applicable
|
||||
if enablements[system.EDBus] {
|
||||
if dbusConfigSession == "builtin" {
|
||||
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
|
||||
} else {
|
||||
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
||||
} else {
|
||||
config.Confinement.SessionBus = c
|
||||
config.Confinement.AppID = aid
|
||||
config.Confinement.Groups = groups
|
||||
config.Confinement.Outer = homeDir
|
||||
config.Confinement.Username = userName
|
||||
|
||||
// enablements from flags
|
||||
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
|
||||
if enablements[i] {
|
||||
config.Confinement.Enablements.Set(i)
|
||||
}
|
||||
}
|
||||
|
||||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
||||
// parse D-Bus config file from flags if applicable
|
||||
if enablements[system.EDBus] {
|
||||
if dbusConfigSession == "builtin" {
|
||||
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
|
||||
} else {
|
||||
config.Confinement.SystemBus = c
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
||||
} else {
|
||||
config.Confinement.SessionBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
||||
} else {
|
||||
config.Confinement.SystemBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// override log from configuration
|
||||
if dbusVerbose {
|
||||
config.Confinement.SessionBus.Log = true
|
||||
config.Confinement.SystemBus.Log = true
|
||||
}
|
||||
}
|
||||
|
||||
// override log from configuration
|
||||
if dbusVerbose {
|
||||
config.Confinement.SessionBus.Log = true
|
||||
config.Confinement.SystemBus.Log = true
|
||||
}
|
||||
}
|
||||
|
||||
// invoke app
|
||||
runApp(app.MustNew(std), config)
|
||||
panic("unreachable")
|
||||
|
||||
// internal commands
|
||||
case "shim":
|
||||
shim.Main()
|
||||
internal.Exit(0)
|
||||
case "init":
|
||||
init0.Main()
|
||||
internal.Exit(0)
|
||||
|
||||
default:
|
||||
log.Fatalf("%q is not a valid command", args[0])
|
||||
// invoke app
|
||||
runApp(app.MustNew(std), config)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
"Path to D-Bus proxy config file, or \"builtin\" for defaults").
|
||||
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
"Path to system D-Bus proxy config file, or \"nil\" to disable").
|
||||
Flag(&mpris, "mpris", command.BoolFlag(false),
|
||||
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
"Force logging in the D-Bus proxy").
|
||||
Flag(&fid, "id", command.StringFlag(""),
|
||||
"App ID, leave empty to disable security context app_id").
|
||||
Flag(&aid, "a", command.IntFlag(0),
|
||||
"Fortify application ID").
|
||||
Flag(nil, "g", &groups,
|
||||
"Groups inherited by the app process").
|
||||
Flag(&homeDir, "d", command.StringFlag("os"),
|
||||
"Application home directory").
|
||||
Flag(&userName, "u", command.StringFlag("chronos"),
|
||||
"Passwd name within sandbox").
|
||||
Flag(&enablements[system.EWayland], "wayland", command.BoolFlag(false),
|
||||
"Allow Wayland connections").
|
||||
Flag(&enablements[system.EX11], "X", command.BoolFlag(false),
|
||||
"Share X11 socket and allow connection").
|
||||
Flag(&enablements[system.EDBus], "dbus", command.BoolFlag(false),
|
||||
"Proxy D-Bus connection").
|
||||
Flag(&enablements[system.EPulse], "pulse", command.BoolFlag(false),
|
||||
"Share PulseAudio socket and cookie")
|
||||
}
|
||||
|
||||
panic("unreachable")
|
||||
var showFlagShort bool
|
||||
c.NewCommand("show", "Show the contents of an app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
||||
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
config, instance := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, showFlagShort, flagJSON)
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
return errSuccess
|
||||
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||
|
||||
var psFlagShort bool
|
||||
c.NewCommand("ps", "List active apps and their state", func(args []string) error {
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
|
||||
c.Command("version", "Show fortify version", func(args []string) error {
|
||||
if v, ok := internal.Check(internal.Version); ok {
|
||||
fmt.Println(v)
|
||||
} else {
|
||||
fmt.Println("impure")
|
||||
}
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("license", "Show full license text", func(args []string) error {
|
||||
fmt.Println(license)
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("template", "Produce a config template", func(args []string) error {
|
||||
printJSON(os.Stdout, false, fst.Template())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("help", "Show this help message", func([]string) error {
|
||||
c.PrintHelp()
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
// internal commands
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
|
||||
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func runApp(a fst.App, config *fst.Config) {
|
||||
|
81
main_test.go
Normal file
81
main_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
)
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: fortify [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
app Launch app defined by the specified config file
|
||||
run Configure and start a permissive default sandbox
|
||||
show Show the contents of an app configuration
|
||||
ps List active apps and their state
|
||||
version Show fortify version
|
||||
license Show full license text
|
||||
template Produce a config template
|
||||
help Show this help message
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
"run", []string{"run", "-h"}, `
|
||||
Usage: fortify run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||
|
||||
Flags:
|
||||
-X Share X11 socket and allow connection
|
||||
-a int
|
||||
Fortify application ID
|
||||
-d string
|
||||
Application home directory (default "os")
|
||||
-dbus
|
||||
Proxy D-Bus connection
|
||||
-dbus-config string
|
||||
Path to D-Bus proxy config file, or "builtin" for defaults (default "builtin")
|
||||
-dbus-log
|
||||
Force logging in the D-Bus proxy
|
||||
-dbus-system string
|
||||
Path to system D-Bus proxy config file, or "nil" to disable (default "nil")
|
||||
-g value
|
||||
Groups inherited by the app process
|
||||
-id string
|
||||
App ID, leave empty to disable security context app_id
|
||||
-mpris
|
||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||
-pulse
|
||||
Share PulseAudio socket and cookie
|
||||
-u string
|
||||
Passwd name within sandbox (default "chronos")
|
||||
-wayland
|
||||
Allow Wayland connections
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := new(bytes.Buffer)
|
||||
c := buildCommand(out)
|
||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||
t.Errorf("Parse: error = %v; want %v",
|
||||
err, command.ErrHelp)
|
||||
}
|
||||
if got := out.String(); got != tc.want {
|
||||
t.Errorf("Parse: %s want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
6
print.go
6
print.go
@ -18,7 +18,7 @@ import (
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
)
|
||||
|
||||
func printShowSystem(output io.Writer, short bool) {
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
@ -43,7 +43,7 @@ func printShowSystem(output io.Writer, short bool) {
|
||||
func printShowInstance(
|
||||
output io.Writer, now time.Time,
|
||||
instance *state.State, config *fst.Config,
|
||||
short bool) {
|
||||
short, flagJSON bool) {
|
||||
if flagJSON {
|
||||
if instance != nil {
|
||||
printJSON(output, short, instance)
|
||||
@ -190,7 +190,7 @@ func printShowInstance(
|
||||
}
|
||||
}
|
||||
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short bool) {
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
||||
var entries state.Entries
|
||||
if e, err := state.Join(s); err != nil {
|
||||
log.Fatalf("cannot join store: %v", err)
|
||||
|
@ -448,14 +448,8 @@ App
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
{
|
||||
v := flagJSON
|
||||
t.Cleanup(func() { flagJSON = v })
|
||||
flagJSON = tc.json
|
||||
}
|
||||
|
||||
output := new(strings.Builder)
|
||||
printShowInstance(output, testTime, tc.instance, tc.config, tc.short)
|
||||
printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
|
||||
if got := output.String(); got != tc.want {
|
||||
t.Errorf("printShowInstance: got\n%s\nwant\n%s",
|
||||
got, tc.want)
|
||||
@ -645,14 +639,8 @@ func Test_printPs(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
{
|
||||
v := flagJSON
|
||||
t.Cleanup(func() { flagJSON = v })
|
||||
flagJSON = tc.json
|
||||
}
|
||||
|
||||
output := new(strings.Builder)
|
||||
printPs(output, testTime, stubStore(tc.entries), tc.short)
|
||||
printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
|
||||
if got := output.String(); got != tc.want {
|
||||
t.Errorf("printPs: got\n%s\nwant\n%s",
|
||||
got, tc.want)
|
||||
|
Loading…
Reference in New Issue
Block a user