diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit new file mode 100644 index 0000000..e37fd09 --- /dev/null +++ b/.github/hooks/pre-commit @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +branch="$(git rev-parse --abbrev-ref HEAD)" + +if [ "$branch" = "master" ]; then + echo "You can't commit directly to master branch" + exit 1 +fi + +if $(awk -F"[<>]" '/EMAIL/ {getline;print $3}' workflow/info.plist | grep -q @); then + echo "You can't commit an email address in the workflow/info.plist (Workflow config)." + exit 1 +fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48adc8a..16d32a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,7 @@ jobs: - name: Copy run dependencies run: | cp -r icons ./workflow + cp -r assets ./workflow go get github.com/pschlump/markdown-cli markdown-cli -i README.md -o workflow/README.html diff --git a/.gitignore b/.gitignore index d49eb7e..890361e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ coverage.txt *.alfredworkflow workflow/bitwarden-alfred-workflow workflow/icons +workflow/assets workflow/README.html diff --git a/Makefile b/Makefile index 8b826d1..89aca83 100644 --- a/Makefile +++ b/Makefile @@ -29,3 +29,8 @@ clean: ## Remove previous build help: ## Display this help screen @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +install-hooks: + @mkdir -p .git/hooks + @cp .github/hooks/* .git/hooks + @chmod +x .git/hooks/* diff --git a/README.md b/README.md index 920e8c7..f2cc4d9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ > NOT tested with Alfred 3 +![Bitwarden V2 - Alfred Workflow Demo](./assets/bitwarden-v2.gif) ## Installation - [Download the latest release](https://github.com/blacs30/bitwarden-alfred-workflow/releases) @@ -110,7 +111,7 @@ Parts of the README are taken over from [alfred-aws-console-services-workflow](h - "I'm seeing the following dialog when running the workflow" - ![image](./icons/catalina-warning.png) + ![image](./assets/catalina-warning.png) Per [the installation steps](https://github.com/blacs30/bitwarden-alfred-workfloww#installation), you **_MUST_** add Alfred to the list of Developer Tool exceptions for Alfred to run any workflow that contains an executable (like this one) diff --git a/assets/bitwarden-v2.gif b/assets/bitwarden-v2.gif new file mode 100644 index 0000000..876d3eb Binary files /dev/null and b/assets/bitwarden-v2.gif differ diff --git a/icons/catalina-warning.png b/assets/catalina-warning.png similarity index 100% rename from icons/catalina-warning.png rename to assets/catalina-warning.png diff --git a/bitwarden.go b/bitwarden.go index 0389321..d084c5d 100644 --- a/bitwarden.go +++ b/bitwarden.go @@ -338,7 +338,7 @@ func runUnlock() { } // remove newline characters password = strings.TrimRight(password, "\r\n") - printOutput([]byte(fmt.Sprintf("first few chars of the password is %s", password[0:2]))) + log.Println("[ERROR] ==> first few chars of the password is ", password[0:2]) // Unlock Bitwarden now message = "Unlocking Bitwarden failed." @@ -358,7 +358,9 @@ func runUnlock() { if err != nil { log.Println(err) } - printError(fmt.Errorf("first few chars of the token is %s", token[0:2])) + if wf.Debug() { + log.Println("[ERROR] ==> first few chars of the token is ", token[0:2]) + } searchAlfred(BW_KEYWORD) fmt.Println("Unlocked") } @@ -369,7 +371,9 @@ func runLogin() { email, sfa, sfaMode, _ := getConfigs(wf) if email == "" { searchAlfred(fmt.Sprintf("%s email", BWCONF_KEYWORD)) - printError(fmt.Errorf("Email missing. Bitwarden not configured yet")) + if wf.Debug() { + log.Println("[ERROR] ==> Email missing. Bitwarden not configured yet") + } wf.Fatal("No email configured.") } @@ -377,13 +381,17 @@ func runLogin() { if loginErr == nil { if unlockErr != nil { searchAlfred(fmt.Sprintf("%s unlock", BWAUTH_KEYWORD)) - printError(fmt.Errorf("Already logged in but locked.")) + if wf.Debug() { + log.Println("[ERROR] ==> Already logged in but locked.") + } wf.Fatal("Already logged in but locked") return } else { searchAlfred(BW_KEYWORD) - printError(fmt.Errorf("Already logged in and unlocked.")) + if wf.Debug() { + log.Println("[ERROR] ==> Already logged in and unlocked.") + } wf.Fatal("Already logged in and unlocked.") } } @@ -400,7 +408,7 @@ func runLogin() { password = passwordReturn[0] } - printOutput([]byte(fmt.Sprintf("first few chars of the password is %s", password[0:2]))) + log.Println(fmt.Sprintf("first few chars of the password is %s", password[0:2])) password = strings.TrimRight(password, "\r\n") args := fmt.Sprintf("%s login %s %s", BwExec, email, password) @@ -438,7 +446,9 @@ func runLogin() { if err != nil { log.Println(err) } - printError(fmt.Errorf("first few chars of the token is %s", token[0:2])) + if wf.Debug() { + log.Println("[ERROR] ==> first few chars of the token is ", token[0:2]) + } searchAlfred(BW_KEYWORD) fmt.Println("Logged In.") } diff --git a/cli.go b/cli.go index 1331559..dc5ab4c 100644 --- a/cli.go +++ b/cli.go @@ -111,7 +111,9 @@ func BitwardenAuthChecks() (loginErr error, unlockErr error) { args = fmt.Sprintf("%s login --check", BwExec) } _, loginErr = runCmd(args, NOT_LOGGED_IN_MSG) - printError(loginErr) + if wf.Debug() { + log.Println("[ERROR] ==> ", loginErr) + } noQuiet := "--quiet" if wf.Debug() { @@ -123,8 +125,10 @@ func BitwardenAuthChecks() (loginErr error, unlockErr error) { } else { args = fmt.Sprintf("%s unlock %s --check --session %s", BwExec, noQuiet, token) } - _, unlockErr = runCmd(args, NOT_LOGGED_IN_MSG) - printError(unlockErr) + _, unlockErr = runCmd(args, NOT_UNLOCKED_MSG) + if wf.Debug() { + log.Println("[ERROR] ==> ", unlockErr) + } return } diff --git a/items.go b/items.go index dd538be..4c686d0 100644 --- a/items.go +++ b/items.go @@ -67,7 +67,7 @@ func addItemDetails(item Item, previousSearch string) { wf.NewItem("Note"). Subtitle(fmt.Sprintf("Secure note: %s", item.Notes)). Icon(iconNote). - Var("notification", "Trying to copy Note"). + Var("notification", "Copy Note"). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-id %s", item.Id)). Arg("notes").Valid(true) // used as jsonpath @@ -89,7 +89,7 @@ func addItemDetails(item Item, previousSearch string) { wf.NewItem(fmt.Sprintf("[Field %d] %s", counter, field.Name)). Subtitle(fmt.Sprintf("%q", field.Value)). Icon(iconBars). - Var("notification", fmt.Sprintf("Trying to copy secret field:\n%s", field.Name)). + Var("notification", fmt.Sprintf("Copy secret field:\n%s", field.Name)). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-id %s", item.Id)). Arg(fmt.Sprintf("fields[%d].value", k)). // used as jsonpath @@ -113,7 +113,7 @@ func addItemDetails(item Item, previousSearch string) { Subtitle(fmt.Sprintf("↩ or ⇥ save Attachment to %s, size %s", outputFolder, att.SizeName)). Icon(iconPaperClip). Valid(true). - Var("notification", fmt.Sprintf("Trying to save attachment to :\n%s%s", outputFolder, att.FileName)). + Var("notification", fmt.Sprintf("Save attachment to :\n%s%s", outputFolder, att.FileName)). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-attachment %s", att.Id)). Var("action3", fmt.Sprintf("-id %s", item.Id)) @@ -188,7 +188,7 @@ func addItemDetails(item Item, previousSearch string) { Subtitle(fmt.Sprintf("%q", item.Login.Password)). Valid(true). Icon(iconPassword). - Var("notification", fmt.Sprintf("Trying to copy Password for user:\n%s", item.Login.Username)). + Var("notification", fmt.Sprintf("Copy Password for user:\n%s", item.Login.Username)). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-id %s", item.Id)). Arg("login.password") // used as jsonpath @@ -199,7 +199,7 @@ func addItemDetails(item Item, previousSearch string) { Subtitle(fmt.Sprintf("%q", item.Login.Totp)). Valid(true). Icon(iconUserClock). - Var("notification", fmt.Sprintf("Trying to copy TOTP for user:\n%s", item.Login.Username)). + Var("notification", fmt.Sprintf("Copy TOTP for user:\n%s", item.Login.Username)). Var("action", "-getitem"). Var("action2", "-totp"). Var("action3", fmt.Sprintf("-id %s", item.Id)) @@ -232,7 +232,7 @@ func addItemDetails(item Item, previousSearch string) { Subtitle(fmt.Sprintf("%q", item.Card.Number)). Valid(true). Icon(iconCreditCard). - Var("notification", fmt.Sprintf("Trying to copy Card Number:\n%s", item.Card.Number)). + Var("notification", fmt.Sprintf("Copy Card Number:\n%s", item.Card.Number)). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-id %s", item.Id)). Arg("card.number") @@ -242,7 +242,7 @@ func addItemDetails(item Item, previousSearch string) { Subtitle(fmt.Sprintf("%q", item.Card.Code)). Valid(true). Icon(iconPassword). - Var("notification", "Trying to copy Card Security Code."). + Var("notification", "Copy Card Security Code."). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-id %s", item.Id)). Arg("card.code") @@ -471,7 +471,7 @@ func addItemsToWorkflow(item Item) { Subtitle(fmt.Sprintf("↩ or ⇥ copy password, %s %s, %s %s %s: Show more", mod1Emoji, item.Login.Username, totp, url, mod4Emoji)).Valid(true). Arg(item.Login.Username). UID(item.Name). - Var("notification", fmt.Sprintf("Trying to copy Password for user:\n%s", item.Login.Username)). + Var("notification", fmt.Sprintf("Copy Password for user:\n%s", item.Login.Username)). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-id %s", item.Id)). Arg("login.password"). @@ -482,7 +482,7 @@ func addItemsToWorkflow(item Item) { Arg(item.Login.Username). Icon(iconUser) if totp != "" { - it1.NewModifier(mod2[0:]...).Subtitle("Trying to copy TOTP"). + it1.NewModifier(mod2[0:]...).Subtitle("Copy TOTP"). Var("action", "-getitem"). Var("action2", "-totp"). Var("action3", fmt.Sprintf("-id %s", item.Id)). @@ -539,7 +539,7 @@ func addItemsToWorkflow(item Item) { Var("action2", fmt.Sprintf("-id %s", item.Id)). Var("notification", fmt.Sprintf("Copied Card %s:\n%s", item.Card.Brand, item.Card.Number)). Arg("card.number") - it3.NewModifier(mod1[0:]...).Subtitle("Trying to copy Card Security Code"). + it3.NewModifier(mod1[0:]...).Subtitle("Copy Card Security Code"). Var("action", "-getitem"). Var("action2", fmt.Sprintf("-id %s", item.Id)). Var("notification", "Copied Card Security Code"). @@ -554,7 +554,6 @@ func addItemsToWorkflow(item Item) { } else { it3.NewModifier(mod4[0:]...).Subtitle("Show item"). Var("action", fmt.Sprintf("-id %s", item.Id)). - Var("action2", fmt.Sprintf("-previous %s", opts.Query)). Arg(""). Icon(iconLink) } diff --git a/utils.go b/utils.go index c058cdb..b73e629 100644 --- a/utils.go +++ b/utils.go @@ -15,24 +15,6 @@ import ( "strings" ) -// prints error in debug mode to console -func printError(err error) { - if wf.Debug() { - if err != nil { - log.Println("[ERROR] ==> " + fmt.Sprintf("%s\n", err.Error())) - } - } -} - -// prints the bytes in debug mode to console -func printOutput(outs []byte) { - if wf.Debug() { - if len(outs) > 0 { - log.Printf("[DEBUG] ==> Output: %s\n", string(outs)) - } - } -} - func transformToItem(input string, target interface{}) error { err := json.Unmarshal([]byte(input), &target) if err != nil { @@ -44,25 +26,45 @@ func transformToItem(input string, target interface{}) error { func checkReturn(status cmd.Status, message string) ([]string, error) { exitCode := status.Exit if exitCode == 127 { - printError(fmt.Errorf("Exit code 127. %q not found in path %q\n", BwExec, os.Getenv("PATH"))) + if wf.Debug() { + log.Printf("[ERROR] ==> Exit code 127. %q not found in path %q\n", BwExec, os.Getenv("PATH")) + } return []string{}, fmt.Errorf("%q not found in path %q\n", BwExec, os.Getenv("PATH")) } else if exitCode == 126 { - printError(fmt.Errorf("Exit code 126. %q has wrong permissions. Must be executable.\n", BwExec)) + if wf.Debug() { + log.Printf("[ERROR] ==> Exit code 126. %q has wrong permissions. Must be executable.\n", BwExec) + } return []string{}, fmt.Errorf("%q has wrong permissions. Must be executable.\n", BwExec) } else if exitCode == 1 { - printError(fmt.Errorf("%s", status.Stderr)) + if wf.Debug() { + log.Println("[ERROR] ==> ", status.Stderr) + } for _, stderr := range status.Stderr { if strings.Contains(stderr, "User cancelled.") { - printError(fmt.Errorf("%s", stderr)) + if wf.Debug() { + log.Println("[ERROR] ==> ", stderr) + } return []string{}, fmt.Errorf("User cancelled.") } } errorString := strings.Join(status.Stderr[:], "") - printError(fmt.Errorf("Exit code 1. %s Err: %s", message, errorString)) + if wf.Debug() { + log.Printf("[ERROR] ==> Exit code 1. %s Err: %s\n", message, errorString) + } return []string{}, fmt.Errorf(fmt.Sprintf("%s Error:\n%s", message, errorString)) } else if exitCode == 0 { return status.Stdout, nil } else { + if wf.Debug() { + log.Println("[DEBUG] Unexpected exit code: => ", exitCode) + // Print each line of STDOUT and STDERR from Cmd + for _, line := range status.Stdout { + log.Println("[DEBUG] Stdout: => ", line) + } + for _, line := range status.Stderr { + log.Println("[DEBUG] Stderr: => ", line) + } + } return []string{}, fmt.Errorf("Unexpected error. Exit code %d.", exitCode) } } @@ -73,12 +75,6 @@ func runCmd(args string, message string) ([]string, error) { runCmd := cmd.NewCmd(argSet[0], argSet[1:]...) status := <-runCmd.Start() - //if wf.Debug() { - // // Print each line of STDOUT from Cmd - // for _, line := range status.Stdout { - // log.Println(line) - // } - //} return checkReturn(status, message) } diff --git a/workflow/info.plist b/workflow/info.plist index 9308048..570ad01 100644 --- a/workflow/info.plist +++ b/workflow/info.plist @@ -3,7 +3,7 @@ bundleid - bitwarden.cli.go + com.lisowski-development.alfred.bitwarden connections 031D762C-EF9F-4DE8-B1FC-764C1DAE4842 @@ -384,7 +384,7 @@ createdby Claas Lisowski description - Rewritten Bitwarden workflow in go + Get passwords, username, TOTP and more from Bitwarden disabled name @@ -998,7 +998,15 @@ readme - Get secrets and other things from Bitwarden. + Get secrets and other things from Bitwarden. + +This workflow aims to give access to all different items from Bitwarden. + +Direct access is given via different modifier keys directly at the search results. Additionally it is possible to show all the details for each items. + +Many of the settings are easily customizable via Workflow Environment Variables. + +Caching of the secret/item names (not the secret values itself) are cached so that the search returns results way faster then version 1.x of the Bitwarden workflow. uidata 031D762C-EF9F-4DE8-B1FC-764C1DAE4842 @@ -1188,7 +1196,7 @@ SERVER_URL version - 2.0.0 + 2.0.1 webaddress https://github.com/blacs30/bitwarden-alfred-workflow