diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a7ab943..10ac012 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,6 +15,8 @@ on:
paths-ignore:
- 'README.md'
- '.github/**'
+ - '.dockerignore'
+ - '.gitignore'
- 'demo**'
- 'go.mod'
- 'go.sum'
@@ -22,11 +24,12 @@ on:
branches:
- main
env:
- VERSION_NUMBER: 'v0.8.0'
- REGISTRY_NAME: digitalghostdev/poke-cli
+ VERSION_NUMBER: 'v0.9.0'
+ DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli'
+ AWS_REGION: 'us-west-2'
jobs:
- snyk:
+ gosec:
runs-on: ubuntu-22.04
permissions:
@@ -38,23 +41,20 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Run Snyk
- uses: snyk/actions/golang@master
- continue-on-error: true
- env:
- SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
+ - name: Run Gosec Security Scanner
+ uses: securego/gosec@master
with:
- args: --sarif-file-output=snyk.sarif --skip-unresolved=true
+ args: '-no-fail -fmt sarif -out results.sarif ./...'
- - name: Upload Result to GitHub Code Scanning
- uses: github/codeql-action/upload-sarif@v2
+ - name: Upload SARIF Report
+ uses: github/codeql-action/upload-sarif@v3
with:
- sarif_file: snyk.sarif
+ sarif_file: results.sarif
build-docker-image:
runs-on: ubuntu-22.04
- needs: [snyk]
- if: needs.snyk.result == 'success'
+ needs: [gosec]
+ if: needs.gosec.result == 'success'
steps:
- name: Checkout
@@ -81,6 +81,33 @@ jobs:
name: poke-cli
path: /tmp/poke-cli.tar
+ # Uploading to Elastic Container Registry has a backup method.
+ upload-to-ecr:
+ runs-on: ubuntu-22.04
+ needs: [build-docker-image]
+ if: needs.build-docker-image.result == 'success'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Configure AWS
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+
+ - name: Login to Amazon ECR
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v2
+
+ - name: Build, tag, and push image to Amazon ECR
+ run : |
+ docker build -t poke-cli:${{ env.VERSION_NUMBER }} .
+ docker tag poke-cli:${{ env.VERSION_NUMBER }} ${{ secrets.AWS_ECR_NAME }}:${{ env.VERSION_NUMBER }}
+ docker push ${{ secrets.AWS_ECR_NAME }}:${{ env.VERSION_NUMBER }}
+
syft:
permissions:
contents: 'read'
@@ -150,8 +177,8 @@ jobs:
architecture-build:
runs-on: ubuntu-22.04
- needs: [snyk]
- if: needs.snyk.result == 'success'
+ needs: [gosec]
+ if: needs.gosec.result == 'success'
strategy:
fail-fast: false
@@ -166,7 +193,7 @@ jobs:
id: meta
uses: 'docker/metadata-action@v5.0.0'
with:
- images: ${{ env.REGISTRY_NAME }}
+ images: ${{ env.DOCKERHUB_REGISTRY_NAME }}
- name: Set up QEMU
uses: 'docker/setup-qemu-action@v3'
@@ -187,7 +214,7 @@ jobs:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
- outputs: type=image,name=${{ env.REGISTRY_NAME }},push-by-digest=true,name-canonical=true,push=true
+ outputs: type=image,name=${{ env.DOCKERHUB_REGISTRY_NAME }},push-by-digest=true,name-canonical=true,push=true
- name: Export Digest
run: |
@@ -232,7 +259,7 @@ jobs:
id: meta
uses: 'docker/metadata-action@v5.0.0'
with:
- images: ${{ env.REGISTRY_NAME }}
+ images: ${{ env.DOCKERHUB_REGISTRY_NAME }}
tags: ${{ env.VERSION_NUMBER }}
- name: Login to Docker Hub
@@ -245,8 +272,8 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
- $(printf '${{ env.REGISTRY_NAME }}@sha256:%s ' *)
+ $(printf '${{ env.DOCKERHUB_REGISTRY_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
- docker buildx imagetools inspect ${{ env.REGISTRY_NAME }}:${{ steps.meta.outputs.version }}
+ docker buildx imagetools inspect ${{ env.DOCKERHUB_REGISTRY_NAME }}:${{ steps.meta.outputs.version }}
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index c8be533..46b9119 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -13,6 +13,8 @@ builds:
- linux
- windows
- darwin
+ ldflags:
+ - -s -w -X main.version=v0.9.0
archives:
- format: tar.gz
diff --git a/Dockerfile b/Dockerfile
index 342cc05..8f6408f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,14 +1,21 @@
-FROM golang:1.23-alpine3.19
+# build 1
+FROM golang:1.23-alpine3.19 AS build
WORKDIR /app
-ENV TERM=xterm-256color
-ENV COLOR_OUTPUT=true
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
-COPY . /app
+RUN go build -ldflags "-X main.version=v0.9.0" -o poke-cli .
-RUN PATH="$PATH:~/go/bin:/usr/local/go/bin:$GOPATH/bin"
+# build 2
+FROM gcr.io/distroless/static-debian12:nonroot
-RUN go install
+COPY --from=build /app/poke-cli /app/poke-cli
+
+ENV TERM=xterm-256color
+ENV COLOR_OUTPUT=true
-ENTRYPOINT ["poke-cli"]
\ No newline at end of file
+ENTRYPOINT ["/app/poke-cli"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 2cd321b..5ec9bb6 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Pokémon CLI
-
+
@@ -19,7 +19,7 @@ My aim is to have four commands finished for `v1.0.0`. Read more in the [Roadmap
---
## Demo
-
+
---
## Install
@@ -68,18 +68,17 @@ _Use a Docker Image_
* Necessary.
```bash
-docker run --rm -i -t digitalghostdev/poke-cli:v0.8.0 [subcommand] flag]
+docker run --rm -i -t digitalghostdev/poke-cli:v0.9.0 [subcommand] flag]
```
### Go Install
-_Install the executable yourself_
+_If you have Go already, install the executable yourself_
-1. Install [Golang](https://go.dev/dl/).
-2. Once installed, run the following command:
+1. Run the following command:
```bash
go install github.com/digitalghost-dev/poke-cli@v0
```
-3. The tool is ready to use!
+2. The tool is ready to use!
---
## Usage
By running `poke-cli [-h | --help]`, it'll display information on how to use the tool.
diff --git a/cli.go b/cli.go
index 488447b..bfccc98 100644
--- a/cli.go
+++ b/cli.go
@@ -7,6 +7,7 @@ import (
"github.com/digitalghost-dev/poke-cli/cmd"
"github.com/digitalghost-dev/poke-cli/flags"
"os"
+ "runtime/debug"
)
var (
@@ -20,10 +21,39 @@ var (
BorderForeground(lipgloss.Color("#F2055C"))
)
+var version = "(devel)"
+
+func currentVersion() {
+ if version != "(devel)" {
+ // Use version injected by -ldflags
+ fmt.Printf("Version: %s\n", version)
+ return
+ }
+
+ // Fallback to build info when version is not set
+ buildInfo, ok := debug.ReadBuildInfo()
+ if !ok {
+ fmt.Println("Version: unknown (unable to read build info)")
+ return
+ }
+
+ if buildInfo.Main.Version != "" {
+ fmt.Printf("Version: %s\n", buildInfo.Main.Version)
+ } else {
+ fmt.Println("Version: (devel)")
+ }
+}
+
func runCLI(args []string) int {
mainFlagSet := flag.NewFlagSet("poke-cli", flag.ContinueOnError)
- latestFlag := mainFlagSet.Bool("latest", false, "Prints the program's latest Docker Image and Release versions.")
- shortLatestFlag := mainFlagSet.Bool("l", false, "Prints the program's latest Docker Image and Release versions.")
+
+ // -l, --latest flag retrieves the latest Docker image and GitHub release versions available
+ latestFlag := mainFlagSet.Bool("latest", false, "Prints the program's latest Docker image and release versions.")
+ shortLatestFlag := mainFlagSet.Bool("l", false, "Prints the program's latest Docker image and release versions.")
+
+ // -v, --version flag retrives the currently installed version
+ currentVersionFlag := mainFlagSet.Bool("version", false, "Prints the current version")
+ shortCurrentVersionFlag := mainFlagSet.Bool("v", false, "Prints the current version")
mainFlagSet.Usage = func() {
helpMessage := helpBorder.Render(
@@ -34,8 +64,8 @@ func runCLI(args []string) int {
fmt.Sprintf("\n\t%-15s %s", "poke-cli [flag]", ""),
"\n\n", styleBold.Render("FLAGS:"),
fmt.Sprintf("\n\t%-15s %s", "-h, --help", "Shows the help menu"),
- fmt.Sprintf("\n\t%-15s %s", "-l, --latest", "Prints the latest available"),
- fmt.Sprintf("\n\t%-15s %s", "", "version of the program"),
+ fmt.Sprintf("\n\t%-15s %s", "-l, --latest", "Prints the latest version available"),
+ fmt.Sprintf("\n\t%-15s %s", "-v, --version", "Prints the current version"),
"\n\n", styleBold.Render("AVAILABLE COMMANDS:"),
fmt.Sprintf("\n\t%-15s %s", "pokemon", "Get details of a specific Pokémon"),
fmt.Sprintf("\n\t%-15s %s", "types", "Get details of a specific typing"),
@@ -70,6 +100,9 @@ func runCLI(args []string) int {
} else if *latestFlag || *shortLatestFlag {
flags.LatestFlag()
return 0
+ } else if *currentVersionFlag || *shortCurrentVersionFlag {
+ currentVersion()
+ return 0
} else if cmdFunc, exists := commands[os.Args[1]]; exists {
cmdFunc()
return 0
diff --git a/cli_test.go b/cli_test.go
index ad5f0d5..852ad8e 100644
--- a/cli_test.go
+++ b/cli_test.go
@@ -110,7 +110,7 @@ func TestRunCLI(t *testing.T) {
{
name: "Latest Flag",
args: []string{"-l"},
- expectedOutput: "Latest Docker image version: v0.7.2\nLatest release tag: v0.7.2\n",
+ expectedOutput: "Latest Docker image version: v0.8.0\nLatest release tag: v0.8.0\n",
expectedCode: 0,
},
}
diff --git a/cmd/types.go b/cmd/types.go
index dd28069..4e5bf5d 100644
--- a/cmd/types.go
+++ b/cmd/types.go
@@ -69,7 +69,7 @@ func displayTypeDetails(typesName string, endpoint string) {
selectedType := cases.Title(language.English).String(typeName)
coloredType := lipgloss.NewStyle().Foreground(lipgloss.Color(getTypeColor(typeName))).Render(selectedType)
- fmt.Printf("You selected the %s type.\nNumber of Pokémon with type: %d\n", coloredType, len(typesStruct.Pokemon))
+ fmt.Printf("You selected the %s type.\nNumber of Pokémon with type: %d\nNumber of moves with type: %d\n", coloredType, len(typesStruct.Pokemon), len(typesStruct.Moves))
fmt.Println("----------")
fmt.Println(styleBold.Render("Damage Chart:"))
diff --git a/connections/connection.go b/connections/connection.go
index df2c12f..003b281 100644
--- a/connections/connection.go
+++ b/connections/connection.go
@@ -2,11 +2,13 @@ package connections
import (
"encoding/json"
+ "errors"
"flag"
"fmt"
"github.com/charmbracelet/lipgloss"
"io"
"net/http"
+ "net/url"
"os"
)
@@ -39,8 +41,12 @@ type PokemonJSONStruct struct {
}
type TypesJSONStruct struct {
- Name string `json:"name"`
- ID int `json:"id"`
+ Name string `json:"name"`
+ ID int `json:"id"`
+ Moves []struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ } `json:"moves"`
Pokemon []struct {
Pokemon struct {
Name string `json:"name"`
@@ -80,8 +86,19 @@ var red = lipgloss.Color("#F2055C")
var errorColor = lipgloss.NewStyle().Foreground(red)
// ApiCallSetup Helper function to handle API calls and JSON unmarshalling
-func ApiCallSetup(url string, target interface{}) error {
- res, err := http.Get(url)
+func ApiCallSetup(rawURL string, target interface{}) error {
+ // Parse and validate the URL
+ parsedURL, err := url.Parse(rawURL)
+ if err != nil {
+ return fmt.Errorf("invalid URL provided: %w", err)
+ }
+
+ // Check the scheme to ensure it's HTTPS
+ if parsedURL.Scheme != "https" {
+ return errors.New("only HTTPS URLs are allowed for security reasons")
+ }
+
+ res, err := http.Get(parsedURL.String())
if err != nil {
return fmt.Errorf("error making GET request: %w", err)
}
diff --git a/flags/version.go b/flags/version.go
index 4d20342..890e574 100644
--- a/flags/version.go
+++ b/flags/version.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
+ "net/url"
"os/exec"
)
@@ -24,7 +25,25 @@ func latestRelease(githubAPIURL string) {
TagName string `json:"tag_name"`
}
- response, err := http.Get(githubAPIURL)
+ // Parse and validate the URL
+ parsedURL, err := url.Parse(githubAPIURL)
+ if err != nil {
+ fmt.Println("Invalid URL:", err)
+ return
+ }
+
+ // Enforce HTTPS and a specific host (e.g., api.github.com)
+ if parsedURL.Scheme != "https" {
+ fmt.Println("Only HTTPS URLs are allowed for security reasons")
+ return
+ }
+ if parsedURL.Host != "api.github.com" {
+ fmt.Println("URL host is not allowed")
+ return
+ }
+
+ // Make the HTTP GET request
+ response, err := http.Get(parsedURL.String())
if err != nil {
fmt.Println("Error fetching data:", err)
return