diff --git a/Makefile b/Makefile index 423a2cb..4034bbf 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # run make help for options # Variables -VERSION=0.3.0 +VERSION=0.4.0 GO=go BUILD_DIR=build BINARY_NAME=opnsense diff --git a/README.MD b/README.MD index a82ac3e..69fa5f0 100644 --- a/README.MD +++ b/README.MD @@ -1,23 +1,26 @@ -# OPNsense CLI +# OPNsense Command Line Interface (CLI) -The **opnsense** CLI is a command-line utility designed to manage, configure, and monitor OPNsense firewall systems. It provides administrators and power users an alternative way to interact with the firewall system without relying on the graphical user interface (GUI). The CLI facilitates both local and remote management via an SSH tunnel, making it versatile for a range of deployment scenarios. Whether it's executing direct configurations, orchestrating complex automation tasks, or troubleshooting on-the-fly, opnsense CLI streamlines these operations through FreeBSD shell commands, employing the same commands used in the Web GUI. Designed with transparency and verifiability in mind, the CLI's shell commands are traceable and each change to the firewall is seeking interactive confirmation (unless supressed with --force flag). Opnsense CLI supports various operating systems including Windows, macOS, Linux, and OpenBSD, allowing creation of administration scripts on either locally on OPNsense system or remotely from admin workstation. +*OPNsense CLI* is a command-line utility for FreeBSD, Linux, MacOS and Windows that empowers administrators and power users to manage, configure, and monitor OPNsense firewall systems. The CLI provides an alternative method to browser-based GUI to interact with the firewall system. -The opnsense CLI can be installed on Windows, macOS, Linux, and OpenBSD, and can target an OPNsense system either locally or remotely. +## Features and Benefits +- *Versatility*: Can operate both locally and remotely (via SSH),and is suitable for various deployment scenarios. +- *Transparency and Control*: All opnsense-cli Commands are bash scripts (and not API calls), with interactive confirmation for changes (bypassable with the --force flag). +- *Cross-Platform Support*: Works with macOS, Windows, Linux, and OpenBSD. +- *Streamlined Operations*: Facilitates repeatable configurations, troubleshooting and complex automations. ## Why Use opnsense CLI? -The OPNsense GUI offers fail-safe but limited configuration capabilities, while the FreeBSD terminal provides direct access to all functionalities of OPNsense but with a greater risk of errors. The `opnsense-cli` utility bridges this gap by: +Administrators typically only two options how to manage the firewall: either through browser-based GUI, or using FreeBSD commands in bash. The opnsense CLI utility provides the middle option by using command line to quickly perform common tasks: -- Allowing a quick view of basic firewall vitals. -- Providing command-line access to a local or remote OPNsense firewall. -- Enabling staged and controlled changes to `conf/config.xml` with rollback options. -- Allowing controlled execution of OPNsense `configctl` commands, calling the same pre-configured commands as the GUI. +- A quick view of firewall settings such as configuration, backups, system vitals. +- Controlled changes of `conf/config.xml`, including staging and rollback options. +- Controlled execution of any OPNsense command available through `configctl`. ## Usage `opnsense [flags] command [parameters]` -### (Planned) Commands +### Commands - **`show system `**: Retrieves system information from the firewall. - **`show config `**: Displays hierarchical segments of `config.xml`. @@ -44,20 +47,35 @@ The OPNsense GUI offers fail-safe but limited configuration capabilities, while ## Installation -BSD12/13 install of opnsense.txz package (into _usr/local/bin_): -`sudo pkg add https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense.txz` - -Signed macos pkg (into _/usr/local/bin_): -`wget https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense-macos.pkg` -`sudo installer -pkg opnsense-macos.pkg -target /` - -Direct download of signed macos opnsense binary: -`wget https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense-macos -O opnsense` -`sudo chmod +x opnsense-macos` - -Ubuntu/Debian opnsense.deb install (into _usr/local/bin_): -`wget https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense.deb` -`dpkg -i opnsense.deb` - -Windows opnsense.exe: -` Invoke-WebRequest -Uri https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense.exe -OutFile opnsense.exe` \ No newline at end of file +### FreeBSD 12/13 + +installs to `usr/local/bin`: +```bash +sudo pkg add https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense.txz +``` +### Debian/Ubuntu + +installs to `usr/local/bin`: +```bash +curl -O https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense.deb +dpkg -i opnsense.deb +``` + +### macOS +Package install (installs to `/usr/local/bin`): +```bash +curl -O https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense.pkg +sudo installer -pkg opnsense.pkg -target / +``` + +signed binary: +```bash +curl -O https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense -O opnsense +sudo chmod +x opnsense +``` + +### Windows + executable `opnsense.exe`: +```powershell +Invoke-WebRequest -Uri https://github.com/mihakralj/opnsense-cli/releases/download/beta/opnsense.exe -OutFile opnsense.exe +``` \ No newline at end of file diff --git a/cmd/backup.go b/cmd/backup.go index 7b6ac96..d4da9f9 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -16,58 +16,54 @@ import ( // backupCmd represents the backup command var backupCmd = &cobra.Command{ Use: "backup", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: + Short: "Lists available backup configurations or a specific backup", + Long: `The 'backup' command allows you to view the available backup configurations in the OPNsense system or retrieve details of a specific backup. It's an essential tool for managing and understanding the saved configurations within your firewall system. -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, +Example usage: +- show backup: Lists all available backup configurations. +- show backup : Displays details of a specific backup configuration identified by .`, Run: func(cmd *cobra.Command, args []string) { - // grab config.xml - configdoc := etree.NewDocument() - bash := fmt.Sprintf("cat %s", configfile) - config, err := internal.ExecuteCmd(bash, host) + backupdir := "/conf/backup/" + path := "backups" + internal.Checkos() + backupdoc := etree.NewDocument() + bash := fmt.Sprintf(`count=$(find %s -type f | wc -l | sed -e 's/^[[:space:]]*//') && echo "\n" && + find /conf/backup -type f -exec sh -c 'echo $(stat -f "%%m" "$1") $(basename "$1") $(stat -f "%%z" "$1") $(md5sum "$1" | cut -c -6)' sh {} \; | sort -nr -k1 | + awk '{ + node = gensub(/[^a-zA-Z0-9_]/, "_", "g", $2); + date = strftime("%%Y-%%m-%%dT%%H:%%M:%%S", $1) + print " <" $2 " date=\"" date "\" size=\"" $3 "\" md5=\"" $4 "\" />";} + END { print ""; }'`, backupdir) + + backups, err := internal.ExecuteCmd(bash, host) if err != nil { internal.Log(1, "execution error: %s", err.Error()) } - err = configdoc.ReadFromString(config) +//fmt.Println(backups) + err = backupdoc.ReadFromString(backups) if err != nil { - internal.Log(1, "%s is not an XML", configfile) + internal.Log(1, "did not receive XML") } - configout := internal.ConfigToXML(configdoc, "") - - // generate backup name - backupdir := "/conf/backup/" - backupfile := generateBackupFilename() - // generate hash + backupout := "" + if xmlFlag { + backupout = internal.ConfigToXML(backupdoc, path) + } else if jsonFlag { + backupout = internal.ConfigToJSON(backupdoc, path) + } else if yamlFlag { + backupout = internal.ConfigToJSON(backupdoc, path) + } else { + backupout = internal.ConfigToTTY(backupdoc, path) + } - // check for existence of backup folder + fmt.Println(backupout) - // pour backup file into the backup folder - bash = fmt.Sprintf("mkdir -p %s&&cat >%s%s <= 1 { + trimmedArg := strings.Trim(args[0], "/") + if matched, _ := regexp.MatchString(`\[0\]`, trimmedArg); matched { + internal.Log(1, "XPath indexing of elements starts with 1, not 0") + } + if trimmedArg != "" { + path = trimmedArg + } + parts := strings.Split(path, "/") + if parts[0] != "opnsense" { + path = "opnsense/" + path + } + } + bash := "" + //internal.Checkos() + configdoc := etree.NewDocument() + bash = fmt.Sprintf("cat %s", configfile) + config, err := internal.ExecuteCmd(bash, host) + if err != nil { + internal.Log(1, "execution error: %s", err.Error()) + } + err = configdoc.ReadFromString(config) + if err != nil { + internal.Log(1, "%s is not an XML", configfile) + } + + stagingdoc := etree.NewDocument() + bash = fmt.Sprintf("if [ -f %s ]; then cat %s; else cat %s; fi", stagingfile, stagingfile, configfile) + staging, err := internal.ExecuteCmd(bash, host) + if err != nil { + internal.Log(1, "execution error: %s", err.Error()) + } + err = stagingdoc.ReadFromString(staging) + if err != nil { + internal.Log(1, "%s is not an XML", stagingfile) + } + + configout := "" + if xmlFlag { + configout = internal.ConfigToXML(configdoc, path) + } else if jsonFlag { + configout = internal.ConfigToJSON(configdoc, path) + } else if yamlFlag { + configout = internal.ConfigToJSON(configdoc, path) + } else { + configout = internal.ConfigToTTY(configdoc, path) + } + + fmt.Println(configout) + + }, +} + +func init() { + showCmd.AddCommand(configCmd) +} diff --git a/cmd/show.go b/cmd/show.go index 98020a4..e83cdc7 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -17,80 +17,33 @@ package cmd import ( "fmt" - "regexp" - "strings" - "github.com/beevik/etree" - "github.com/mihakralj/opnsense/internal" "github.com/spf13/cobra" ) // showCmd represents the show command var showCmd = &cobra.Command{ - Use: "show", - Short: "Displays information about configuration stored in config.xml", - Long: `The show command displays configuration elements in config.xml, including interfaces, routes, firewall rules, and other system settings. - Use this command to view the current system configuration and troubleshoot issues.`, + Use: "show [node]", + Short: "Display information related to OPNsense system", + Long: `The 'show' command retrieves various details about the OPNsense system: +- show system : Retrieves system information from the firewall. +- show config : Displays hierarchical segments of config.xml. +- show backup : Lists available backup configs or a specific backup.`, Run: func(cmd *cobra.Command, args []string) { - - path := "opnsense" - if len(args) >= 1 { - trimmedArg := strings.Trim(args[0], "/") - if matched, _ := regexp.MatchString(`\[0\]`, trimmedArg); matched { - internal.Log(1, "XPath indexing of elements starts with 1, not 0") - } - if trimmedArg != "" { - path = trimmedArg - } - parts := strings.Split(path, "/") - if parts[0] != "opnsense" { - path = "opnsense/" + path - } - } - bash := "" - //internal.Checkos() - configdoc := etree.NewDocument() - bash = fmt.Sprintf("cat %s", configfile) - config, err := internal.ExecuteCmd(bash, host) - if err != nil { - internal.Log(1, "execution error: %s", err.Error()) - } - err = configdoc.ReadFromString(config) - if err != nil { - internal.Log(1, "%s is not an XML", configfile) - } - - stagingdoc := etree.NewDocument() - bash = fmt.Sprintf("if [ -f %s ]; then cat %s; else cat %s; fi", stagingfile, stagingfile, configfile) - staging, err := internal.ExecuteCmd(bash, host) - if err != nil { - internal.Log(1, "execution error: %s", err.Error()) - } - err = stagingdoc.ReadFromString(staging) - if err != nil { - internal.Log(1, "%s is not an XML", stagingfile) - } - - if false { - fmt.Println(stagingdoc) - } - - configout := "" - if xmlFlag { - configout = internal.ConfigToXML(configdoc, path) - } else if jsonFlag { - configout = internal.ConfigToJSON(configdoc, path) - } else if yamlFlag { - configout = internal.ConfigToJSON(configdoc, path) - } else { - configout = internal.ConfigToTTY(configdoc, path) - } - - fmt.Println(configout) - + fmt.Println("Specify a subcommand such as 'system', 'config', or 'backup'") }, } func init() { rootCmd.AddCommand(showCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // showCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // showCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/info.go b/cmd/system.go similarity index 89% rename from cmd/info.go rename to cmd/system.go index 3644285..87f73f6 100644 --- a/cmd/info.go +++ b/cmd/system.go @@ -1,6 +1,5 @@ /* Copyright © 2023 NAME HERE - */ package cmd @@ -13,9 +12,9 @@ import ( "github.com/spf13/cobra" ) -// infoCmd represents the status command -var infoCmd = &cobra.Command{ - Use: "info [node]", +// systemCmd represents the status command +var systemCmd = &cobra.Command{ + Use: "system [node]", Short: "Retrieves system information", Long: ` Info command provides a comprehensive overview of the OPNsense's current state. @@ -32,6 +31,10 @@ opnsense info storage/disk0 opnsense info network/igb0/mtu `, Run: func(cmd *cobra.Command, args []string) { + if changed := cmd.Flags().Changed("depth"); !changed { + depth = 2 + internal.SetFlags(verbose, force, host, configfile, nocolor, depth, xmlFlag, yamlFlag, jsonFlag) + } path := "system" if len(args) >= 1 { @@ -71,15 +74,5 @@ opnsense info network/igb0/mtu } func init() { - rootCmd.AddCommand(infoCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // statusCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // statusCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + showCmd.AddCommand(systemCmd) } diff --git a/internal/checkos.go b/internal/checkos.go index 1e0b17b..0101eaa 100644 --- a/internal/checkos.go +++ b/internal/checkos.go @@ -1,7 +1,6 @@ package internal import ( - "fmt" "strings" ) @@ -14,7 +13,7 @@ func Checkos() (string, error) { } osstr = strings.TrimSpace(osstr) if osstr != "FreeBSD OPNsense" { - return "", fmt.Errorf("the target system is not FreeBSD OPNsense") + Log(1, "%s is not OPNsense system", osstr) } Log(4, "OPNsense detected") return osstr, nil diff --git a/internal/parser.go b/internal/parser.go index 1421cb5..22502ed 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -96,7 +96,7 @@ func EtreeToTTY(el *etree.Element, level int, indent int) string { // attributes attributestr := "" for _, attr := range el.Attr { - attributestr = fmt.Sprintf(c["ita"]+c["blu"]+" (%s=\"%s\")"+c["nil"], attr.Key, attr.Value) + attributestr += fmt.Sprintf(c["ita"]+c["blu"]+" (%s=\"%s\")"+c["nil"], attr.Key, attr.Value) } if len(el.ChildElements()) > 0 { result.WriteString(fmt.Sprintf("%s%s: {", indentation, el.Tag)) diff --git a/internal/ssh.go b/internal/ssh.go index d01f892..89032d0 100644 --- a/internal/ssh.go +++ b/internal/ssh.go @@ -51,7 +51,7 @@ func getSSHClient(target string) (*SSHClient, error) { } if len(config.Auth) == 0 { - fmt.Println("No suitable SSH identities found in ssh-agent.\nFor enhanced security add SSH key to the ssh agent") + fmt.Println("No suitable SSH identities found in ssh-agent.\nFor enhanced security add SSH key to the ssh agent using ssh-add command") fmt.Printf("Enter password for %s@%s: \n", user, host) bytePassword, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println()