From 874857a0c84039ef284ccb9d7b7f82ca6967be4e Mon Sep 17 00:00:00 2001 From: mr-pmillz <> Date: Tue, 3 Sep 2024 18:25:17 -0400 Subject: [PATCH] initial poc --- .github/workflows/ci.yml | 99 ++++++++++++++++++++++++++ .gitignore | 10 +++ .golangci-lint.yml | 60 ++++++++++++++++ .goreleaser.yml | 42 +++++++++++ Makefile | 55 +++++++++++++++ README.md | 37 ++++++++++ cliff.toml | 115 ++++++++++++++++++++++++++++++ cmd/eoldate/main.go | 146 +++++++++++++++++++++++++++++++++++++++ eoldate.go | 86 +++++++++++++++++++++++ go.mod | 30 ++++++++ go.sum | 68 ++++++++++++++++++ logging.go | 33 +++++++++ utils.go | 85 +++++++++++++++++++++++ 13 files changed, 866 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .golangci-lint.yml create mode 100644 .goreleaser.yml create mode 100644 Makefile create mode 100644 cliff.toml create mode 100644 cmd/eoldate/main.go create mode 100644 eoldate.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logging.go create mode 100644 utils.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f7180cb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI + +# Controls when the action will run. Triggers the workflow on push with tags +on: + push: + tags: + - '*' + pull_request: + +permissions: + contents: write + # packages: write + # issues: write + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # The "build" workflow + build: + # The type of runner that the job will run on + strategy: + matrix: + go-version: [1.21, 1.22, 1.23] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + # Setup Go + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + # Run build of the application + - name: Run build + run: | + go env -w GOFLAGS=-mod=mod + go mod tidy + go build -v -o eoldate ./cmd/eoldate/main.go + golangci: + needs: build + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v5 + with: + go-version: 1.22 + - uses: actions/checkout@v4 + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + # Optional: golangci-lint command line arguments. + args: --config ./.golangci-lint.yml + # ================ + # RELEASE JOB + # runs after a successful build + # only runs on push "*" tag + # ================ + release: + needs: [build, golangci] + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Setup Go + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 + cache: true + + - name: Set GOPATH + run: | + echo "GOPATH=$(go env GOPATH)/bin" >> $GITHUB_ENV + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + if: startsWith(github.ref, 'refs/tags/v') + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload assets + uses: actions/upload-artifact@v4 + with: + name: eoldate + path: dist/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ec8be4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea +.idea/ +.idea/* +*.txt +*.sh +*.yaml +*.json +*.log +*.csv +eoldate diff --git a/.golangci-lint.yml b/.golangci-lint.yml new file mode 100644 index 0000000..d51b972 --- /dev/null +++ b/.golangci-lint.yml @@ -0,0 +1,60 @@ +run: + timeout: 5m + +linters: + disable-all: true + enable: + - bodyclose + #- copyloopvar + #- deadcode + #- depguard + #- dogsled + - dupl + - errorlint + #- exportloopref + #- exhaustive + #- funlen + #- gas + #- gochecknoinits + - goconst + - gocognit + - gocritic + #- gocyclo + #- gofmt + #- goimports + #- golint + #- gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + #- interfacer + #- lll + #- maligned + #- misspell + #- nakedret + #- noctx + #- nolintlint + # - prealloc + # - revive + #- rowserrcheck + #- scopelint + - staticcheck + #- structcheck + - stylecheck + - typecheck + - unconvert + #- unparam + - unused + #- varcheck + - whitespace + fast: false + +linters-settings: + gosimple: + checks: ["all", "-S1028"] + staticcheck: + checks: ["all"] + gosec: + checks: ["all", "-G306"] diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..1f78e25 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,42 @@ +env: + - GO111MODULE=on +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + id: eoldate + binary: eoldate + main: cmd/eoldate/main.go + flags: + - -trimpath + asmflags: + - all=-trimpath={{.Env.GOPATH}} + gcflags: + - all=-trimpath={{.Env.GOPATH}} + ldflags: | + -s -w + goos: + - linux + goarch: + - amd64 + - arm64 + +archives: + - id: tgz + format: tar.gz + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: 'checksums.txt' + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a34cb78 --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +BIN="./bin" +SRC=$(shell find . -name "*.go") +CURRENT_TAG=$(shell git describe --tags --abbrev=0) + +GOLANGCI := $(shell command -v golangci-lint 2>/dev/null) +RICHGO := $(shell command -v richgo 2>/dev/null) +GOTESTFMT := $(shell command -v gotestfmt 2>/dev/null) + +.PHONY: fmt lint build test changelog + +default: all + +all: fmt lint build test changelog + +fmt: + $(info ******************** checking formatting ********************) + @test -z $(shell gofmt -l $(SRC)) || (gofmt -d $(SRC); exit 1) + +.PHONY: golangci-lint-check +golangci-lint-check: +ifndef GITHUB_ACTIONS + $(info ******************** checking if golangci-lint is installed ********************) + $(warning "ensuring latest version of golangci-lint installed, running: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest") + go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest +endif + +.PHONY: lint +lint: golangci-lint-check + $(info ******************** running lint tools ********************) + golangci-lint run -c .golangci-lint.yml -v ./... --timeout 10m + +test: + $(info ******************** running tests ********************) + ifeq ($(GITHUB_ACTIONS), true) + ifndef GOTESTFMT + $(warning "could not find gotestfmt in $(PATH), running: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest") + $(shell go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest) + endif + go test -json -v ./... 2>&1 | tee coverage/gotest.log | gotestfmt + else + ifndef RICHGO + $(warning "could not find richgo in $(PATH), running: go install github.com/kyoh86/richgo@latest") + $(shell go install github.com/kyoh86/richgo@latest) + endif + richgo test -v ./... + endif + +changelog: + $(info ******************** running git-cliff updating CHANGELOG.md ********************) + git-cliff -o CHANGELOG.md + +build: + go env -w GOFLAGS=-mod=mod + go mod tidy + go build -v -o eoldate -trimpath -ldflags="-s -w" ./cmd/eoldate diff --git a/README.md b/README.md index f76c1d4..e028279 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ # eoldate + +## About + +An End of Life Date API SDK written in Go + +This is a fairly simple wrapper around the endoflife.date API +[Read the Docs](https://endoflife.date/docs/api) + +## Installation + +To install, just run the below command or download pre-compiled binary from the [releases page](https://github.com/mr-pmillz/eoldate/releases) + +```bash +go install -v github.com/mr-pmillz/eoldate/cmd/eoldate@latest +``` + +## eoldate as a library + +Integrate eoldate with other go programs + +```go +package main + +import ( + "fmt" + "github.com/mr-pmillz/eoldate" +) + +func main() { + client := eoldate.NewClient(eoldate.EOLBaseURL) + releaseVersions, err := client.GetProduct("php") + if err != nil { + panic(err) + } + fmt.Println(releaseVersions) +} +``` \ No newline at end of file diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..b98a87f --- /dev/null +++ b/cliff.toml @@ -0,0 +1,115 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + +[remote.github] +owner = "mr-pmillz" +repo = "eoldate" + +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented [here](https://github.com/mr-pmillz/eoldate/blob/main/CHANGELOG.md?ref_type=heads) \n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% macro print_commit(commit) -%} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }} - \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/-/commit/{{ commit.id }}))\ +{% endmacro -%} + +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]\ + ({{ self::remote_url() }}/-/compare/{{ previous.version }}...{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits + | filter(attribute="scope") + | sort(attribute="scope") %} + {{ self::print_commit(commit=commit) }} + {%- endfor -%} + {% raw %}\n{% endraw %}\ + {%- for commit in commits %} + {%- if not commit.scope -%} + {{ self::print_commit(commit=commit) }} + {% endif -%} + {% endfor -%} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing whitespace from the templates +trim = true +# postprocessors +postprocessors = [] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = false +# filter out the commits that are not conventional +filter_unconventional = false +# process each line of a commit as an individual commit +split_commits = true +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^.*: add", group = "โœจ New features" }, + { message = "^add", group = "โœจ New features" }, + { message = "^feat*", group = "โœจ: New features" }, + { message = "^.* support", group = "โœจ New features" }, + { message = "^fix*", group = "๐Ÿ› Bug fixes" }, + { message = "^.* resolved", group = "๐Ÿ› Bug fixes" }, + { message = "^.*: fix", group = "๐Ÿ› Bug fixes" }, + { message = "^refactor*", group = "๐Ÿšœ Refactor" }, + { message = "^doc", group = "๐Ÿ“š Documentation" }, + { message = "^perf", group = "โšก Performance" }, + { message = "^optimized", group = "โšก Performance" }, + { message = "^enhanced", group = "โšก Performance" }, + { message = "^style", group = "๐ŸŽจ Styling" }, + { message = "^test*", group = "๐Ÿงช Testing" }, + { message = "^deps", group = "๐Ÿ“ฆ Dependencies" }, + { message = "^dependencies", group = "๐Ÿ“ฆ Dependencies" }, + { message = "^dependency", group = "๐Ÿ“ฆ Dependencies" }, + { message = "^update*", group = "๐Ÿ›  Improvements" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^.*", group = "โš™๏ธ Miscellaneous" }, + { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks" }, + { body = ".*security", group = "๐Ÿ›ก๏ธ Security" }, + { message = "^revert", group = "โ—€๏ธ Revert" }, + { message = "^.*: remove", group = "Removed" }, + { message = "^.*: delete", group = "Removed" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# regex for matching git tags +tag_pattern = "v[0-9].*" + +# regex for skipping tags +skip_tags = "beta|alpha" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "newest" diff --git a/cmd/eoldate/main.go b/cmd/eoldate/main.go new file mode 100644 index 0000000..2086a8f --- /dev/null +++ b/cmd/eoldate/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "flag" + "fmt" + "github.com/mr-pmillz/eoldate" + "github.com/olekukonko/tablewriter" + "github.com/projectdiscovery/gologger" + "os" + "reflect" + "strings" + "time" +) + +func main() { + tech := flag.String("t", "", "technology/software name to lookup") + output := flag.String("o", "", "output directory to save results to") + version := flag.Bool("version", false, "show version and exit") + getAll := flag.Bool("getall", false, "get all results from all technologies") + flag.Parse() + + eolOptions := eoldate.Options{} + if *output != "" { + absOutputDir, err := eoldate.ResolveAbsPath(*output) + if err != nil { + gologger.Fatal().Msg(err.Error()) + } + eolOptions.Output = absOutputDir + if err = os.MkdirAll(absOutputDir, 0755); err != nil { + gologger.Fatal().Msg(err.Error()) + } + } + + eolOptions.Tech = *tech + eolOptions.Version = *version + eolOptions.GetAll = *getAll + + if eolOptions.Version { + fmt.Printf("Version: %s\n", eoldate.CurrentVersion) + os.Exit(0) + } + + client := eoldate.NewClient(eoldate.EOLBaseURL) + + if *getAll { + gologger.Info().Msg("Getting all available technologies") + allProducts, err := client.GetAllProducts() + if err != nil { + gologger.Fatal().Msg(err.Error()) + } + fmt.Println(allProducts) + os.Exit(0) + } + + releaseVersions, err := client.GetProduct(eolOptions.Tech) + if err != nil { + fmt.Printf("Error fetching product data: %v\n", err) + return + } + // Create a new table + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + table.SetHeader([]string{"Cycle", "Release Date", "EOL Date", "Latest", "Latest Release Date", "LTS", "Support"}) + + table.SetHeaderColor( + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, + ) + // Get current date + currentDate := time.Now() + + row := make([]string, 0) + var eolDateTime time.Time + // Add data to the table + for _, release := range releaseVersions { + var EOLIsBool bool + rt := reflect.TypeOf(release.EOL) + if rt.Kind() == reflect.String { + eolDateTime, err = time.Parse("2006-01-02", release.EOL.(string)) + if err != nil { + fmt.Println("Error parsing date:", err) + continue + } + row = []string{release.Cycle, release.ReleaseDate, release.EOL.(string), release.Latest, release.LatestReleaseDate, fmt.Sprintf("%t", release.LTS), release.Support} + } + if rt.Kind() == reflect.Bool { + EOLIsBool = true + row = []string{release.Cycle, release.ReleaseDate, "N/A", release.Latest, release.LatestReleaseDate, fmt.Sprintf("%t", release.LTS), release.Support} + } + + // Check if EOL date is older or later than the current date + if eolDateTime.Before(currentDate) && !EOLIsBool { + table.Rich(row, []tablewriter.Colors{{}, {}, tablewriter.Colors{tablewriter.FgRedColor}, {}, {}, {}, {}}) + } else { + table.Rich(row, []tablewriter.Colors{{}, {}, tablewriter.Colors{tablewriter.FgGreenColor}, {}, {}, {}, {}}) + } + } + + table.SetColumnAlignment([]int{ + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + tablewriter.ALIGN_CENTER, + }) + + table.SetAutoWrapText(true) + table.SetRowLine(true) + table.SetFooter([]string{ + strings.ToUpper(eolOptions.Tech), + "", + "", + "", + "", + "", + "", + }) + table.SetFooterAlignment(tablewriter.ALIGN_LEFT) + + // Render the table + table.Render() + fmt.Println(tableString.String()) + + if eolOptions.Output != "" { + tableOutputFile := fmt.Sprintf("%s/%s.txt", eolOptions.Output, eolOptions.Tech) + softwareEolDateCSV := fmt.Sprintf("%s/%s.csv", eolOptions.Output, eolOptions.Tech) + softwareEolDateJSON := fmt.Sprintf("%s/%s.json", eolOptions.Output, eolOptions.Tech) + + if err = eoldate.WriteStringToFile(tableOutputFile, tableString.String()); err != nil { + gologger.Fatal().Msg(err.Error()) + } + if err = eoldate.WriteStructToJSONFile(releaseVersions, softwareEolDateJSON); err != nil { + gologger.Fatal().Msg(err.Error()) + } + if err = eoldate.WriteStructToCSVFile(releaseVersions, softwareEolDateCSV); err != nil { + gologger.Fatal().Msg(err.Error()) + } + } +} diff --git a/eoldate.go b/eoldate.go new file mode 100644 index 0000000..8bd89d6 --- /dev/null +++ b/eoldate.go @@ -0,0 +1,86 @@ +package eoldate + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +const CurrentVersion = `v0.0.1` +const EOLBaseURL = "https://endoflife.date/api" + +// Options ... +type Options struct { + Tech string + Output string + Version bool + GetAll bool +} + +// Product represents the structure of the JSON data +type Product struct { + Cycle string `json:"cycle,omitempty"` + ReleaseDate string `json:"releaseDate,omitempty"` + EOL interface{} `json:"eol,omitempty"` + Latest string `json:"latest,omitempty"` + LatestReleaseDate string `json:"latestReleaseDate,omitempty"` + LTS bool `json:"lts,omitempty"` + Support string `json:"support,omitempty"` +} + +type AllProducts []string + +// Client is the API client for the endoflife.date API. +type Client struct { + httpClient *http.Client + baseURL string +} + +// NewClient creates a new API client with the given base URL. +func NewClient(baseURL string) *Client { + return &Client{ + httpClient: &http.Client{}, + baseURL: baseURL, + } +} + +// Get fetches data from a given endpoint. +func (c *Client) Get(endpoint string) ([]byte, error) { + url := fmt.Sprintf("%s/%s", c.baseURL, endpoint) + resp, err := c.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch data: %s", resp.Status) + } + + return io.ReadAll(resp.Body) +} + +// GetProduct fetches the end-of-life information for a specific product. +func (c *Client) GetProduct(product string) ([]Product, error) { + data, err := c.Get(fmt.Sprintf("%s.json", product)) + if err != nil { + return nil, LogError(err) + } + + products := make([]Product, 0) + err = json.Unmarshal(data, &products) + return products, err +} + +// GetAllProducts fetches the end-of-life information for all products. +func (c *Client) GetAllProducts() (AllProducts, error) { + data, err := c.Get("all.json") + if err != nil { + return nil, LogError(err) + } + + all := make(AllProducts, 0) + err = json.Unmarshal(data, &all) + return all, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fd8927e --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module github.com/mr-pmillz/eoldate + +go 1.22.1 + +require ( + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 + github.com/olekukonko/tablewriter v0.0.5 + github.com/projectdiscovery/gologger v1.1.22 +) + +require ( + github.com/andybalholm/brotli v1.0.6 // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mholt/archiver/v3 v3.5.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nwaples/rardecode v1.1.3 // indirect + github.com/pierrec/lz4/v4 v4.1.2 // indirect + github.com/projectdiscovery/utils v0.2.7 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/ulikunitz/xz v0.5.11 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + gopkg.in/djherbis/times.v1 v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7d60c7d --- /dev/null +++ b/go.sum @@ -0,0 +1,68 @@ +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= +github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/projectdiscovery/gologger v1.1.22 h1:wMj1vZEsGOJbPI1zE21OpgZWLMmU5l8M2EawOXjusIs= +github.com/projectdiscovery/gologger v1.1.22/go.mod h1:1Ty1drRfXKy+uMTHsoAF8f9n94qjDJWZy5h9PJWiqlU= +github.com/projectdiscovery/utils v0.2.7 h1:XWdz7SscL++jqsnQ9ecHzSZE0RK33tyPcnqcXw+vmKs= +github.com/projectdiscovery/utils v0.2.7/go.mod h1:N0N7tbdNFPegd9NpJ3onCPClaBrERcOIB88yww6UCF8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= +gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..4472dc9 --- /dev/null +++ b/logging.go @@ -0,0 +1,33 @@ +package eoldate + +import ( + "fmt" + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/gologger/formatter" + "github.com/projectdiscovery/gologger/levels" + "os" + "runtime" + "time" +) + +// LogError ... +func LogError(err error) error { + timestamp := time.Now().Format("01-02-2006") + fname := fmt.Sprintf("eoldate-error-log-%s.json", timestamp) + + f, openFileErr := os.OpenFile(fname, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if openFileErr != nil { + return openFileErr + } + defer f.Close() + teeFormatter := formatter.NewTee(formatter.NewCLI(false), f) + gologger.DefaultLogger.SetFormatter(teeFormatter) + pc, file, line, ok := runtime.Caller(1) + if !ok { + gologger.Warning().Msg("Failed to retrieve Caller information") + } + fn := runtime.FuncForPC(pc).Name() + gologger.DefaultLogger.SetMaxLevel(levels.LevelError) + gologger.Error().Msgf("Error in function %s, called from %s:%d:\n %v", fn, file, line, err) + return err +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..a5b9d53 --- /dev/null +++ b/utils.go @@ -0,0 +1,85 @@ +package eoldate + +import ( + "encoding/json" + "github.com/gocarina/gocsv" + "os" + "os/user" + "path/filepath" + "strings" +) + +// WriteStructToJSONFile ... +func WriteStructToJSONFile(data interface{}, outputFile string) error { + outputFileDir := filepath.Dir(outputFile) + if err := os.MkdirAll(outputFileDir, 0750); err != nil { + return LogError(err) + } + + f, err := json.MarshalIndent(data, "", " ") + if err != nil { + return LogError(err) + } + + if err = os.WriteFile(outputFile, f, 0644); err != nil { //nolint:gosec + return LogError(err) + } + return nil +} + +// WriteStructToCSVFile ... +func WriteStructToCSVFile(data interface{}, outputFile string) error { + outputFileDir := filepath.Dir(outputFile) + if err := os.MkdirAll(outputFileDir, 0750); err != nil { + return LogError(err) + } + + file, err := os.OpenFile(outputFile, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return LogError(err) + } + defer file.Close() + + err = gocsv.MarshalFile(data, file) + if err != nil { + return LogError(err) + } + + return nil +} + +// WriteStringToFile writes a string to a file +func WriteStringToFile(outputFile, data string) error { + out, err := os.Create(outputFile) + if err != nil { + return err + } + defer out.Close() + if _, err = out.WriteString(data); err != nil { + return LogError(err) + } + + return nil +} + +// ResolveAbsPath ... +func ResolveAbsPath(path string) (string, error) { + usr, err := user.Current() + if err != nil { + return path, LogError(err) + } + + dir := usr.HomeDir + if path == "~" { + path = dir + } else if strings.HasPrefix(path, "~/") { + path = filepath.Join(dir, path[2:]) + } + + path, err = filepath.Abs(path) + if err != nil { + return path, LogError(err) + } + + return path, nil +}