package main

import (
	"bufio"
	"encoding/json"
	"encoding/xml"
	"flag"
	"fmt"
	"golang.org/x/crypto/ssh"
	"log"
	"os"
	"os/exec"
	"sigs.k8s.io/yaml"
	"strings"
)

var usage = `
  Usage: %s [OPTION] [<file> ...]

  Collects info from authorized_keys files from every user it can find

  OPTIONS:

      -j  --json     output JSON
      -J  --jsonl    output JSONLINES
      -r  --rec      output RECUTILS
      -x  --xml      output XML
      -y  --yaml     output YAML

          --version  show version information

  ARGUMENTS:

      file           authorized_keys file
`

var options struct {
	version bool
	json    bool
	jsonl   bool
	xml     bool
	rec     bool
	yaml    bool
}

type User struct {
	Name      string
	Password  string
	UID       string
	GID       string
	GECOS     string
	Directory string
	Shell     string
}

type AuthorizedKey struct {
	Name              string
	Directory         string
	File              string
	Type              string
	FingerprintMD5    string
	FingerprintSHA256 string
	Comment           string
	Options           []string
	IsCA              bool
}

var Users []User
var authorizedKeys []AuthorizedKey

var Version string = "ssh-authorized-keys (ssh-tools) 1.9"

func printVersion() {
	fmt.Printf("%s\n", strings.TrimSpace(Version))
}

func getent_passwd() {

	cmd := exec.Command("getent", "passwd")

	stdout, err := cmd.StdoutPipe()

	if err != nil {
		log.Println(err)
	}

	scanner := bufio.NewScanner(stdout)

	err = cmd.Start()

	if err != nil {
		log.Println(err)
	}

	for scanner.Scan() {
		line := scanner.Text()
		// skip all line starting with #
		if equal := strings.Index(line, "#"); equal < 0 {

			lineSlice := strings.Split(line, ":")

			if len(lineSlice) > 0 {
				user := User{}
				user.Name = lineSlice[0]
				user.Password = lineSlice[1]
				user.UID = lineSlice[2]
				user.GID = lineSlice[3]
				user.GECOS = lineSlice[4]
				user.Directory = lineSlice[5]
				//user.Shell = lineSlice[6]

				Users = append(Users, user)

			}

		}
	}

	if scanner.Err() != nil {
		cmd.Process.Kill()
		cmd.Wait()
		fmt.Println(scanner.Err())
	}

	cmd.Wait()
}

func get_authorized_keys(authorized_keys_file string, name string, directory string) {

	_, err := os.Open(authorized_keys_file)

	if err == nil {

		authorizedKeysBytes, err := os.ReadFile(authorized_keys_file)

		if err != nil {
			log.Println(err)
		}

		for len(authorizedKeysBytes) > 0 {
			pubKey, comment, options, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)

			authorizedKeysBytes = rest

			if err != nil {
				log.Println(err, "in", authorized_keys_file)
				continue
			}

			isCA := false

			for _, option := range options {
				if option == "cert-authority" {
					isCA = true
				}
			}

			authorizedKey := AuthorizedKey{}

			authorizedKey.Name = name
			authorizedKey.Directory = directory
			authorizedKey.File = authorized_keys_file
			authorizedKey.Type = pubKey.Type()
			authorizedKey.FingerprintMD5 = ssh.FingerprintLegacyMD5(pubKey)
			authorizedKey.FingerprintSHA256 = ssh.FingerprintSHA256(pubKey)
			authorizedKey.Comment = comment
			authorizedKey.Options = options
			authorizedKey.IsCA = isCA

			authorizedKeys = append(authorizedKeys, authorizedKey)

		}

	}

}

func main() {

	// CLI
	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(), usage, os.Args[0])
	}

	flag.BoolVar(&options.version, "version", options.version, "Show version information")

	flag.BoolVar(&options.json, "j", options.json, "Output JSON")
	flag.BoolVar(&options.json, "json", options.json, "Output JSON")

	flag.BoolVar(&options.jsonl, "J", options.jsonl, "Output JSONLINES")
	flag.BoolVar(&options.jsonl, "jsonl", options.jsonl, "Output JSONLINES")

	flag.BoolVar(&options.rec, "r", options.rec, "Output RECUTILS")
	flag.BoolVar(&options.rec, "rec", options.rec, "Output RECUTILS")

	flag.BoolVar(&options.xml, "x", options.xml, "Output XML")
	flag.BoolVar(&options.xml, "xml", options.xml, "Output XML")

	flag.BoolVar(&options.yaml, "y", options.yaml, "Output YAML")
	flag.BoolVar(&options.yaml, "yaml", options.yaml, "Output YAML")

	flag.Parse()

	if options.version {
		printVersion()
		os.Exit(0)
	}

	files := flag.Args()

	find_all_files := true

	if len(files) > 0 {

		find_all_files = false

	}

	if find_all_files {

		// Get all users
		getent_passwd()

		// Now we have a list of users
		// Iterate over each of them to
		// get their info and authorized_keys file

		for _, u := range Users {

			authorized_keys_file := u.Directory + "/.ssh/authorized_keys"

			get_authorized_keys(authorized_keys_file, u.Name, u.Directory)

		}

	} else {

		for _, f := range files {

			authorized_keys_file := f

			get_authorized_keys(authorized_keys_file, "N/A", "N/A")

		}

	}

	// XML Output
	if options.xml {
		xmlBytes, err := xml.MarshalIndent(authorizedKeys, "", "    ")

		if err != nil {
			log.Println(err)
		}

		xmlOutput := string(xmlBytes)

		fmt.Println(xmlOutput)

		os.Exit(0)
	}

	// JSON Output
	if options.json {
		jsonBytes, err := json.MarshalIndent(authorizedKeys, "", "    ")

		if err != nil {
			log.Println(err)
		}

		jsonOutput := string(jsonBytes)

		fmt.Println(jsonOutput)

		os.Exit(0)
	}

	// JSON Lines Output
	if options.jsonl {

		for _, authorizedKey := range authorizedKeys {
			jsonBytes, err := json.Marshal(authorizedKey)

			if err != nil {
				log.Println(err)
			}

			jsonOutput := string(jsonBytes)

			fmt.Println(jsonOutput)
		}

		os.Exit(0)
	}

	// YAML Output
	if options.yaml {
		yamlBytes, err := yaml.Marshal(authorizedKeys)

		if err != nil {
			log.Println(err)
		}

		yamlOutput := string(yamlBytes)

		fmt.Println(yamlOutput)
		os.Exit(0)
	}

	// RECUTILS Output
	if options.rec {
		for _, authKey := range authorizedKeys {
			fmt.Println("")
			fmt.Printf("%s: %v\n", "Name", authKey.Name)
			fmt.Printf("%s: %v\n", "Directory", authKey.Directory)
			fmt.Printf("%s: %v\n", "File", authKey.File)
			fmt.Printf("%s: %v\n", "Type", authKey.Type)
			fmt.Printf("%s: %v\n", "FingerprintMD5", authKey.FingerprintMD5)
			fmt.Printf("%s: %v\n", "FingerprintSHA256", authKey.FingerprintSHA256)
			fmt.Printf("%s: %v\n", "Comment", authKey.Comment)
			fmt.Printf("%s: %v\n", "Options", authKey.Options)
			fmt.Printf("%s: %v\n", "IsCA", authKey.IsCA)
		}

		os.Exit(0)
	}

	// DEFAULT Output
	for _, authKey := range authorizedKeys {
		fmt.Println("")

		fmt.Printf("%-20s : %v\n", "Name", authKey.Name)
		fmt.Printf("%-20s : %v\n", "Directory", authKey.Directory)
		fmt.Printf("%-20s : %v\n", "File", authKey.File)
		fmt.Printf("%-20s : %v\n", "Type", authKey.Type)
		fmt.Printf("%-20s : %v\n", "FingerprintMD5", authKey.FingerprintMD5)
		fmt.Printf("%-20s : %v\n", "FingerprintSHA256", authKey.FingerprintSHA256)
		fmt.Printf("%-20s : %v\n", "Comment", authKey.Comment)
		fmt.Printf("%-20s : %v\n", "Options", authKey.Options)
		fmt.Printf("%-20s : %v\n", "IsCA", authKey.IsCA)

	}
	os.Exit(0)

}
