From 23af466f3e6b626a38ef981f87c9ab7a1b072e81 Mon Sep 17 00:00:00 2001 From: dobarx <111326505+dobarx@users.noreply.github.com> Date: Sat, 30 Mar 2024 12:02:40 +0200 Subject: [PATCH] internal/nistnvd: implement nist_nvd_cves data source (#137) --- .goreleaser-dev.yaml | 6 + .goreleaser.yaml | 24 ++ .mockery.yaml | 4 + docs/plugins/nist_nvd/_index.md | 30 ++ .../nist_nvd/data-sources/nist_nvd_cves.md | 70 +++++ docs/plugins/plugins.json | 213 +++++++++------ examples/templates/nist_nvd/example.fabric | 38 +++ internal/nistnvd/client/client.go | 121 ++++++++ internal/nistnvd/client/client_test.go | 136 +++++++++ internal/nistnvd/cmd/main.go | 14 + internal/nistnvd/data_nist_nvd_cves.go | 258 ++++++++++++++++++ internal/nistnvd/data_nist_nvd_cves_test.go | 199 ++++++++++++++ internal/nistnvd/plugin.go | 20 ++ internal/nistnvd/plugin_test.go | 14 + internal/plugin_validity_test.go | 2 + mocks/internalpkg/nistnvd/client/client.go | 97 +++++++ plugin/resolver/source.go | 2 +- plugin/resolver/source_remote.go | 6 +- plugin/resolver/source_remote_test.go | 2 +- tools/docgen/content-provider.md.gotempl | 4 + tools/docgen/data-source.md.gotempl | 4 + tools/docgen/main.go | 2 + tools/docgen/plugin.md.gotempl | 4 + 23 files changed, 1178 insertions(+), 92 deletions(-) create mode 100644 docs/plugins/nist_nvd/_index.md create mode 100644 docs/plugins/nist_nvd/data-sources/nist_nvd_cves.md create mode 100644 examples/templates/nist_nvd/example.fabric create mode 100644 internal/nistnvd/client/client.go create mode 100644 internal/nistnvd/client/client_test.go create mode 100644 internal/nistnvd/cmd/main.go create mode 100644 internal/nistnvd/data_nist_nvd_cves.go create mode 100644 internal/nistnvd/data_nist_nvd_cves_test.go create mode 100644 internal/nistnvd/plugin.go create mode 100644 internal/nistnvd/plugin_test.go create mode 100644 mocks/internalpkg/nistnvd/client/client.go diff --git a/.goreleaser-dev.yaml b/.goreleaser-dev.yaml index eb7f37b6..433e1459 100644 --- a/.goreleaser-dev.yaml +++ b/.goreleaser-dev.yaml @@ -51,6 +51,12 @@ builds: ldflags: "-X main.version={{.Version}}" no_unique_dist_dir: true + - id: nistnvd + main: ./internal/nistnvd/cmd + binary: "plugins/blackstork/nist_nvd@{{ .Version }}" + ldflags: "-X main.version={{.Version}}" + no_unique_dist_dir: true + - id: postgresql main: ./internal/postgresql/cmd binary: "plugins/blackstork/postgresql@{{ .Version }}" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c831094d..904c89f8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -77,6 +77,18 @@ builds: - windows - darwin + - id: plugin_nist_nvd + main: ./internal/nistnvd/cmd + binary: "nist_nvd@{{ .Version }}" + flags: "-trimpath" + hooks: + post: + - go run ./tools/pluginmeta --namespace blackstork --version {{.Version}} patch --plugin {{.Path}} --os {{.Os}} --arch {{.Arch}} + goos: + - linux + - windows + - darwin + - id: plugin_opencti main: ./internal/opencti/cmd binary: "opencti@{{ .Version }}" @@ -336,6 +348,18 @@ archives: {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} + - id: plugin_nist_nvd + format: tar.gz + builds: + - plugin_nist_nvd + name_template: >- + plugin_nist_nvd_ + {{- .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + changelog: sort: asc filters: diff --git a/.mockery.yaml b/.mockery.yaml index e2df577c..6cebffd0 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -29,6 +29,10 @@ packages: config: interfaces: Client: + github.com/blackstork-io/fabric/internal/nistnvd/client: + config: + interfaces: + Client: github.com/blackstork-io/fabric/plugin/resolver: config: inpackage: true diff --git a/docs/plugins/nist_nvd/_index.md b/docs/plugins/nist_nvd/_index.md new file mode 100644 index 00000000..0fc788ef --- /dev/null +++ b/docs/plugins/nist_nvd/_index.md @@ -0,0 +1,30 @@ +--- +title: blackstork/nist_nvd +weight: 20 +plugin: + name: blackstork/nist_nvd + description: "" + tags: [] + version: "v0.4.0" + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/nistnvd/" +type: docs +--- + +{{< plugin-header "blackstork/nist_nvd" "nist_nvd" "v0.4.0" >}} + +## Installation + +To install the plugin, add it to `plugin_versions` map in the Fabric global configuration block (see [Global configuration]({{< ref "configs.md#global-configuration" >}}) for more details), with a version constraint restricting which available versions of the plugin the codebase is compatible with: + +```hcl +fabric { + plugin_versions = { + "blackstork/nist_nvd" = ">= v0.4.0" + } +} +``` + + +## Data sources + +{{< plugin-resources "nist_nvd" "data-source" >}} diff --git a/docs/plugins/nist_nvd/data-sources/nist_nvd_cves.md b/docs/plugins/nist_nvd/data-sources/nist_nvd_cves.md new file mode 100644 index 00000000..09ed835f --- /dev/null +++ b/docs/plugins/nist_nvd/data-sources/nist_nvd_cves.md @@ -0,0 +1,70 @@ +--- +title: nist_nvd_cves +plugin: + name: blackstork/nist_nvd + description: "" + tags: [] + version: "v0.4.0" + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/nistnvd/" +resource: + type: data-source +type: docs +--- + +{{< breadcrumbs 2 >}} + +{{< plugin-resource-header "blackstork/nist_nvd" "nist_nvd" "v0.4.0" "nist_nvd_cves" "data source" >}} + +## Installation + +To use `nist_nvd_cves` data source, you must install the plugin `blackstork/nist_nvd`. + +To install the plugin, add the full plugin name to the `plugin_versions` map in the Fabric global configuration block (see [Global configuration]({{< ref "configs.md#global-configuration" >}}) for more details), as shown below: + +```hcl +fabric { + plugin_versions = { + "blackstork/nist_nvd" = ">= v0.4.0" + } +} +``` + +Note the version constraint set for the plugin. + +## Configuration + +The data source supports the following configuration parameters: + +```hcl +config data nist_nvd_cves { + api_key = # optional +} +``` + +## Usage + +The data source supports the following parameters in the data blocks: + +```hcl +data nist_nvd_cves { + cpe_name = # optional + cve_id = # optional + cvss_v3_metrics = # optional + cvss_v3_severity = # optional + cwe_id = # optional + has_cert_alerts = # optional + has_cert_notes = # optional + has_kev = # optional + is_vulnerable = # optional + keyword_exact_match = # optional + keyword_search = # optional + last_mod_end_date = # optional + last_mod_start_date = # optional + limit = # optional + no_rejected = # optional + pub_end_date = # optional + pub_start_date = # optional + source_identifier = # optional + virtual_match_string = # optional +} +``` \ No newline at end of file diff --git a/docs/plugins/plugins.json b/docs/plugins/plugins.json index 0895adbd..29c062cc 100644 --- a/docs/plugins/plugins.json +++ b/docs/plugins/plugins.json @@ -2,6 +2,10 @@ { "name": "blackstork/builtin", "resources": [ + { + "name": "inline", + "type": "data-source" + }, { "name": "csv", "type": "data-source", @@ -27,32 +31,34 @@ ] }, { - "name": "inline", - "type": "data-source" + "name": "title", + "type": "content-provider", + "arguments": [ + "value", + "absolute_size", + "relative_size" + ] }, { - "name": "toc", + "name": "image", "type": "content-provider", "arguments": [ - "start_level", - "end_level", - "ordered", - "scope" + "src", + "alt" ] }, { - "name": "text", + "name": "table", "type": "content-provider", "arguments": [ - "value" + "columns" ] }, { - "name": "code", + "name": "blockquote", "type": "content-provider", "arguments": [ - "value", - "language" + "value" ] }, { @@ -67,39 +73,33 @@ "name": "frontmatter", "type": "content-provider", "arguments": [ - "content", - "format" + "format", + "content" ] }, { - "name": "title", + "name": "toc", "type": "content-provider", "arguments": [ - "value", - "absolute_size", - "relative_size" + "scope", + "start_level", + "end_level", + "ordered" ] }, { - "name": "blockquote", + "name": "text", "type": "content-provider", "arguments": [ "value" ] }, { - "name": "image", - "type": "content-provider", - "arguments": [ - "src", - "alt" - ] - }, - { - "name": "table", + "name": "code", "type": "content-provider", "arguments": [ - "columns" + "language", + "value" ] } ], @@ -113,24 +113,24 @@ "name": "elasticsearch", "type": "data-source", "config_params": [ + "cloud_id", + "api_key_str", "api_key", "basic_auth_username", "basic_auth_password", "bearer_auth", "ca_certs", - "base_url", - "cloud_id", - "api_key_str" + "base_url" ], "arguments": [ + "only_hits", "fields", "size", "index", "id", "query_string", "query", - "aggs", - "only_hits" + "aggs" ] } ], @@ -147,17 +147,17 @@ "github_token" ], "arguments": [ + "repository", + "milestone", + "state", + "creator", "mentioned", "sort", "direction", "since", "limit", - "repository", - "milestone", - "creator", - "labels", - "state", - "assignee" + "assignee", + "labels" ] } ], @@ -189,13 +189,13 @@ "name": "openai_text", "type": "content-provider", "config_params": [ + "system_prompt", "api_key", - "organization_id", - "system_prompt" + "organization_id" ], "arguments": [ - "prompt", - "model" + "model", + "prompt" ] } ], @@ -281,52 +281,52 @@ "api_token" ], "arguments": [ - "closed_at__gt", - "disclosed_at__lt", - "bounty_awarded_at__lt", - "created_at__gt", - "submitted_at__gt", - "submitted_at__lt", - "bounty_awarded_at__gt", - "last_report_activity_at__gt", - "reporter", - "id", - "disclosed_at__null", - "last_program_activity_at__gt", - "last_program_activity_at__null", - "last_public_activity_at__lt", - "inbox_ids", - "triaged_at__null", - "closed_at__null", + "weakness_id", "hacker_published", "triaged_at__lt", - "last_public_activity_at__gt", - "first_program_activity_at__lt", - "first_program_activity_at__null", + "size", + "program", + "id", + "triaged_at__gt", + "closed_at__lt", + "disclosed_at__lt", "last_program_activity_at__lt", "last_activity_at__gt", - "keyword", + "reporter_agreed_on_going_public", "bounty_awarded_at__null", + "swag_awarded_at__gt", "swag_awarded_at__null", - "last_report_activity_at__lt", - "first_program_activity_at__gt", - "custom_fields", + "last_public_activity_at__lt", + "keyword", "assignee", - "weakness_id", - "triaged_at__gt", + "swag_awarded_at__lt", + "first_program_activity_at__null", + "last_program_activity_at__gt", + "last_public_activity_at__gt", + "custom_fields", "state", - "severity", - "program", + "created_at__gt", "created_at__lt", - "closed_at__lt", "disclosed_at__gt", - "reporter_agreed_on_going_public", - "size", + "disclosed_at__null", + "bounty_awarded_at__lt", "page_number", + "submitted_at__lt", + "triaged_at__null", + "closed_at__gt", + "bounty_awarded_at__gt", + "first_program_activity_at__lt", + "last_activity_at__lt", + "inbox_ids", + "severity", + "submitted_at__gt", + "last_report_activity_at__gt", "sort", - "swag_awarded_at__gt", - "swag_awarded_at__lt", - "last_activity_at__lt" + "reporter", + "closed_at__null", + "last_report_activity_at__lt", + "first_program_activity_at__gt", + "last_program_activity_at__null" ] } ], @@ -343,10 +343,10 @@ "api_key" ], "arguments": [ - "end_date", - "user_id", "group_id", - "start_date" + "start_date", + "end_date", + "user_id" ] } ], @@ -365,12 +365,12 @@ "deployment_name" ], "arguments": [ + "search_query", "max_count", "status_buckets", "rf", "earliest_time", - "latest_time", - "search_query" + "latest_time" ] } ], @@ -384,21 +384,56 @@ "name": "stixview", "type": "content-provider", "arguments": [ - "show_labels", - "height", + "stix_url", "caption", + "show_footer", "show_tlp_as_tags", - "show_marking_nodes", - "show_sidebar", "show_idrefs", "width", "gist_id", - "stix_url", - "show_footer" + "show_marking_nodes", + "show_labels", + "height", + "show_sidebar" ] } ], "shortname": "stixview", "version": "v0.4.0" + }, + { + "name": "blackstork/nist_nvd", + "resources": [ + { + "name": "nist_nvd_cves", + "type": "data-source", + "config_params": [ + "api_key" + ], + "arguments": [ + "cvss_v3_metrics", + "source_identifier", + "cvss_v3_severity", + "virtual_match_string", + "is_vulnerable", + "last_mod_start_date", + "pub_start_date", + "pub_end_date", + "cve_id", + "has_cert_alerts", + "has_kev", + "keyword_exact_match", + "limit", + "last_mod_end_date", + "cpe_name", + "cwe_id", + "keyword_search", + "has_cert_notes", + "no_rejected" + ] + } + ], + "shortname": "nist_nvd", + "version": "v0.4.0" } ] \ No newline at end of file diff --git a/examples/templates/nist_nvd/example.fabric b/examples/templates/nist_nvd/example.fabric new file mode 100644 index 00000000..7a96e2a0 --- /dev/null +++ b/examples/templates/nist_nvd/example.fabric @@ -0,0 +1,38 @@ +# Example of working with NIST NVD CVES + +fabric { + plugin_versions = { + "blackstork/nist_nvd" = ">= 0.4.1 < 1.0 || 0.4.1-rev0" + "blackstork/openai" = ">= 0.4.1 < 1.0 || 0.4.1-rev0" + } +} + +document "example" { + title = "CVE-2024-29018 NIST NVD CVE vulnerability" + + data nist_nvd_cves "cves" { + cve_id = "CVE-2024-29018" + } + + section { + title = "Description" + content openai_text { + config { + api_key = from_env_variable("OPENAI_API_KEY") + } + query = ".data.nist_nvd_cves.cves" + prompt = "Short description of NIST CVE vulnerability." + } + } + section { + title = "What to do?" + content openai_text { + config { + api_key = from_env_variable("OPENAI_API_KEY") + } + query = ".data.nist_nvd_cves.cves" + prompt = "Step by step guide how to mitigate NIST CVE vulnerability." + } + } + +} \ No newline at end of file diff --git a/internal/nistnvd/client/client.go b/internal/nistnvd/client/client.go new file mode 100644 index 00000000..e858bad5 --- /dev/null +++ b/internal/nistnvd/client/client.go @@ -0,0 +1,121 @@ +package client + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "time" + + "github.com/google/go-querystring/query" +) + +func String(s string) *string { + return &s +} + +func Bool(b bool) BoolValue { + return BoolValue(b) +} + +func Int(i int) *int { + return &i +} + +type BoolValue bool + +func (b BoolValue) EncodeValues(key string, v *url.Values) error { + if b { + v.Add(key, "") + } + return nil +} + +type ListCVESReq struct { + ResultsPerPage int `url:"resultsPerPage"` + StartIndex int `url:"startIndex"` + CPEName *string `url:"cpeName,omitempty"` + LastModStartDate *string `url:"lastModStartDate,omitempty"` + LastModEndDate *string `url:"lastModEndDate,omitempty"` + PubStartDate *string `url:"pubStartDate,omitempty"` + PubEndDate *string `url:"pubEndDate,omitempty"` + VirtualMatchString *string `url:"virtualMatchString,omitempty"` + CVEID *string `url:"cveId,omitempty"` + CVSSV3Metrics *string `url:"cvssV3Metrics,omitempty"` + CVSSV3Severity *string `url:"cvssV3Severity,omitempty"` + CWEID *string `url:"cweId,omitempty"` + HasCertAlerts BoolValue `url:"hasCertAlerts,omitempty"` + HasCertNotes BoolValue `url:"hasCertNotes,omitempty"` + HasKev BoolValue `url:"hasKev,omitempty"` + IsVulnerable BoolValue `url:"isVulnerable,omitempty"` + NoRejected BoolValue `url:"noRejected,omitempty"` + KeywordSearch *string `url:"keywordSearch,omitempty"` + KeywordExactMatch BoolValue `url:"keywordExactMatch,omitempty"` + SourceIdentifier *string `url:"sourceIdentifier,omitempty"` +} + +type ListCVESRes struct { + ResultsPerPage int `json:"resultsPerPage"` + StartIndex int `json:"startIndex"` + TotalResults int `json:"totalResults"` + Vulnerabilities []any `json:"vulnerabilities"` +} + +type Client interface { + ListCVES(ctx context.Context, req *ListCVESReq) (*ListCVESRes, error) +} + +const ( + defaultBaseURL = "https://services.nvd.nist.gov" +) + +type client struct { + url string + apiKey *string +} + +func (c *client) auth(r *http.Request) { + if c.apiKey != nil { + q := r.URL.Query() + q.Set("apiKey", *c.apiKey) + r.URL.RawQuery = q.Encode() + } +} + +func New(apiKey *string) Client { + return &client{ + url: defaultBaseURL, + apiKey: apiKey, + } +} + +func (c *client) ListCVES(ctx context.Context, req *ListCVESReq) (*ListCVESRes, error) { + u, err := url.Parse(c.url + "/rest/json/cves/2.0") + if err != nil { + return nil, err + } + q, err := query.Values(req) + if err != nil { + return nil, err + } + u.RawQuery = q.Encode() + r, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + r.Header.Set("Accept", "application/json") + c.auth(r) + client := http.Client{ + Timeout: 15 * time.Second, + } + res, err := client.Do(r) + if err != nil { + return nil, err + } + defer res.Body.Close() + var data ListCVESRes + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/internal/nistnvd/client/client_test.go b/internal/nistnvd/client/client_test.go new file mode 100644 index 00000000..c7ad77f7 --- /dev/null +++ b/internal/nistnvd/client/client_test.go @@ -0,0 +1,136 @@ +package client + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type ClientTestSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc +} + +func (s *ClientTestSuite) SetupTest() { + s.ctx, s.cancel = context.WithCancel(context.Background()) +} + +func (s *ClientTestSuite) TearDownTest() { + s.cancel() +} + +func TestClientTestSuite(t *testing.T) { + suite.Run(t, new(ClientTestSuite)) +} + +func (s *ClientTestSuite) mock(fn http.HandlerFunc, apiKey *string) (Client, *httptest.Server) { + srv := httptest.NewServer(fn) + cli := &client{ + url: srv.URL, + apiKey: apiKey, + } + return cli, srv +} + +func hasQueryKey(q *http.Request, key string) bool { + _, ok := q.URL.Query()[key] + return ok +} + +func (s *ClientTestSuite) TestAuth() { + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + s.Equal("api_key", r.URL.Query().Get("apiKey")) + }, String("api_key")) + defer srv.Close() + client.ListCVES(s.ctx, &ListCVESReq{}) +} + +func (s *ClientTestSuite) TestListCVES() { + ts := time.Unix(123, 0).UTC().Format(time.RFC3339) + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + s.Equal("/rest/json/cves/2.0", r.URL.Path) + s.Equal(http.MethodGet, r.Method) + s.Equal("10", r.URL.Query().Get("resultsPerPage")) + s.Equal("1", r.URL.Query().Get("startIndex")) + s.Equal("cpe:2.3:o:microsoft:windows_10:1607", r.URL.Query().Get("cpeName")) + s.True(hasQueryKey(r, "isVulnerable")) + s.Equal(ts, r.URL.Query().Get("lastModStartDate")) + s.Equal(ts, r.URL.Query().Get("lastModEndDate")) + s.Equal(ts, r.URL.Query().Get("pubStartDate")) + s.Equal(ts, r.URL.Query().Get("pubEndDate")) + s.Equal("virtual", r.URL.Query().Get("virtualMatchString")) + s.Equal("cve-2021-1234", r.URL.Query().Get("cveId")) + s.Equal("cvssv3", r.URL.Query().Get("cvssV3Metrics")) + s.Equal("high", r.URL.Query().Get("cvssV3Severity")) + s.Equal("cwe-123", r.URL.Query().Get("cweId")) + s.True(hasQueryKey(r, "hasCertAlerts")) + s.True(hasQueryKey(r, "hasCertNotes")) + s.True(hasQueryKey(r, "hasKev")) + s.True(hasQueryKey(r, "noRejected")) + w.Write([]byte(`{ + "resultsPerPage": 10, + "startIndex": 1, + "totalResults": 1, + "vulnerabilities": [ + { + "any": "data" + } + ] + }`)) + }, nil) + defer srv.Close() + req := ListCVESReq{ + ResultsPerPage: 10, + StartIndex: 1, + CPEName: String("cpe:2.3:o:microsoft:windows_10:1607"), + IsVulnerable: Bool(true), + LastModStartDate: String(ts), + LastModEndDate: String(ts), + PubStartDate: String(ts), + PubEndDate: String(ts), + VirtualMatchString: String("virtual"), + CVEID: String("cve-2021-1234"), + CVSSV3Metrics: String("cvssv3"), + CVSSV3Severity: String("high"), + CWEID: String("cwe-123"), + HasCertAlerts: Bool(true), + HasCertNotes: Bool(true), + HasKev: Bool(true), + NoRejected: Bool(true), + KeywordSearch: String("keyword"), + KeywordExactMatch: Bool(true), + SourceIdentifier: String("source"), + } + result, err := client.ListCVES(s.ctx, &req) + s.NoError(err) + s.Equal(&ListCVESRes{ + ResultsPerPage: 10, + StartIndex: 1, + TotalResults: 1, + Vulnerabilities: []any{ + map[string]any{ + "any": "data", + }, + }, + }, result) +} + +func (s *ClientTestSuite) TestListCVESError() { + client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, nil) + defer srv.Close() + req := ListCVESReq{} + _, err := client.ListCVES(s.ctx, &req) + s.Error(err) +} + +func (s *ClientTestSuite) TestDefaultClientURL() { + cli := New(nil) + s.Equal("https://services.nvd.nist.gov", cli.(*client).url) +} diff --git a/internal/nistnvd/cmd/main.go b/internal/nistnvd/cmd/main.go new file mode 100644 index 00000000..763cec73 --- /dev/null +++ b/internal/nistnvd/cmd/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/blackstork-io/fabric/internal/nistnvd" + pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" +) + +var version string + +func main() { + pluginapiv1.Serve( + nistnvd.Plugin(version, nistnvd.DefaultClientLoader), + ) +} diff --git a/internal/nistnvd/data_nist_nvd_cves.go b/internal/nistnvd/data_nist_nvd_cves.go new file mode 100644 index 00000000..53fe44f4 --- /dev/null +++ b/internal/nistnvd/data_nist_nvd_cves.go @@ -0,0 +1,258 @@ +package nistnvd + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + + "github.com/blackstork-io/fabric/internal/nistnvd/client" + "github.com/blackstork-io/fabric/plugin" +) + +const ( + defaultLimit = 1000 + minLimit = 1 + maxLimit = 2000 +) + +func makeNistNvdCvesDataSource(loader ClientLoadFn) *plugin.DataSource { + return &plugin.DataSource{ + DataFunc: fetchNistNvdCvesData(loader), + Config: hcldec.ObjectSpec{ + "api_key": &hcldec.AttrSpec{ + Name: "api_key", + Type: cty.String, + Required: false, + }, + }, + Args: hcldec.ObjectSpec{ + "last_mod_start_date": &hcldec.AttrSpec{ + Name: "last_mod_start_date", + Type: cty.String, + Required: false, + }, + "last_mod_end_date": &hcldec.AttrSpec{ + Name: "last_mod_end_date", + Type: cty.String, + Required: false, + }, + "pub_start_date": &hcldec.AttrSpec{ + Name: "pub_start_date", + Type: cty.String, + Required: false, + }, + "pub_end_date": &hcldec.AttrSpec{ + Name: "pub_end_date", + Type: cty.String, + Required: false, + }, + "cpe_name": &hcldec.AttrSpec{ + Name: "cpe_name", + Type: cty.String, + Required: false, + }, + "cve_id": &hcldec.AttrSpec{ + Name: "cve_id", + Type: cty.String, + Required: false, + }, + "cvss_v3_metrics": &hcldec.AttrSpec{ + Name: "cvss_v3_metrics", + Type: cty.String, + Required: false, + }, + "cvss_v3_severity": &hcldec.AttrSpec{ + Name: "cvss_v3_severity", + Type: cty.String, + Required: false, + }, + "cwe_id": &hcldec.AttrSpec{ + Name: "cwe_id", + Type: cty.String, + Required: false, + }, + "keyword_search": &hcldec.AttrSpec{ + Name: "keyword_search", + Type: cty.String, + Required: false, + }, + "virtual_match_string": &hcldec.AttrSpec{ + Name: "virtual_match_string", + Type: cty.String, + Required: false, + }, + "source_identifier": &hcldec.AttrSpec{ + Name: "source_identifier", + Type: cty.String, + Required: false, + }, + "has_cert_alerts": &hcldec.AttrSpec{ + Name: "has_cert_alerts", + Type: cty.Bool, + Required: false, + }, + "has_kev": &hcldec.AttrSpec{ + Name: "has_kev", + Type: cty.Bool, + Required: false, + }, + "has_cert_notes": &hcldec.AttrSpec{ + Name: "has_cert_notes", + Type: cty.Bool, + Required: false, + }, + "is_vulnerable": &hcldec.AttrSpec{ + Name: "is_vulnerable", + Type: cty.Bool, + Required: false, + }, + "keyword_exact_match": &hcldec.AttrSpec{ + Name: "keyword_exact_match", + Type: cty.Bool, + Required: false, + }, + "no_rejected": &hcldec.AttrSpec{ + Name: "no_rejected", + Type: cty.Bool, + Required: false, + }, + "limit": &hcldec.AttrSpec{ + Name: "limit", + Type: cty.Number, + Required: false, + }, + }, + } +} + +func fetchNistNvdCvesData(loader ClientLoadFn) plugin.RetrieveDataFunc { + return func(ctx context.Context, params *plugin.RetrieveDataParams) (plugin.Data, hcl.Diagnostics) { + cli, err := parseConfig(params.Config, loader) + if err != nil { + return nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Failed to parse configuration", + }} + } + req, err := parseListCVESRequest(params.Args) + if err != nil { + return nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Failed to parse arguments", + }} + } + limit := defaultLimit + if attr := params.Args.GetAttr("limit"); !attr.IsNull() { + num, _ := attr.AsBigFloat().Int64() + limit = int(num) + if limit < minLimit { + limit = minLimit + } else if limit > maxLimit { + limit = maxLimit + } + } + req.ResultsPerPage = limit + req.StartIndex = 0 + var vulnerabilities plugin.ListData + for { + res, err := cli.ListCVES(ctx, req) + if err != nil { + return nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Failed to fetch data", + }} + } + for _, v := range res.Vulnerabilities { + data, err := plugin.ParseDataAny(v) + if err != nil { + return nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Failed to parse data", + }} + } + vulnerabilities = append(vulnerabilities, data) + } + if res.StartIndex+res.ResultsPerPage >= res.TotalResults { + break + } + req.StartIndex = res.StartIndex + res.ResultsPerPage + } + return vulnerabilities, nil + } +} + +func parseConfig(cfg cty.Value, loader ClientLoadFn) (client.Client, error) { + if cfg.IsNull() { + return nil, fmt.Errorf("configuration is required") + } + apiKey := cfg.GetAttr("api_key") + if apiKey.IsNull() || apiKey.AsString() == "" { + return loader(nil), nil + } + return loader(client.String(apiKey.AsString())), nil +} + +func parseListCVESRequest(args cty.Value) (*client.ListCVESReq, error) { + if args.IsNull() { + return nil, fmt.Errorf("arguments are required") + } + req := &client.ListCVESReq{} + if attr := args.GetAttr("last_mod_start_date"); !attr.IsNull() { + req.LastModStartDate = client.String(attr.AsString()) + } + if attr := args.GetAttr("last_mod_end_date"); !attr.IsNull() { + req.LastModEndDate = client.String(attr.AsString()) + } + if attr := args.GetAttr("pub_start_date"); !attr.IsNull() { + req.PubStartDate = client.String(attr.AsString()) + } + if attr := args.GetAttr("pub_end_date"); !attr.IsNull() { + req.PubEndDate = client.String(attr.AsString()) + } + if attr := args.GetAttr("cpe_name"); !attr.IsNull() { + req.CPEName = client.String(attr.AsString()) + } + if attr := args.GetAttr("cve_id"); !attr.IsNull() { + req.CVEID = client.String(attr.AsString()) + } + if attr := args.GetAttr("cvss_v3_metrics"); !attr.IsNull() { + req.CVSSV3Metrics = client.String(attr.AsString()) + } + if attr := args.GetAttr("cvss_v3_severity"); !attr.IsNull() { + req.CVSSV3Severity = client.String(attr.AsString()) + } + if attr := args.GetAttr("cwe_id"); !attr.IsNull() { + req.CWEID = client.String(attr.AsString()) + } + if attr := args.GetAttr("keyword_search"); !attr.IsNull() { + req.KeywordSearch = client.String(attr.AsString()) + } + if attr := args.GetAttr("virtual_match_string"); !attr.IsNull() { + req.VirtualMatchString = client.String(attr.AsString()) + } + if attr := args.GetAttr("source_identifier"); !attr.IsNull() { + req.SourceIdentifier = client.String(attr.AsString()) + } + if attr := args.GetAttr("has_cert_alerts"); !attr.IsNull() { + req.HasCertAlerts = client.Bool(attr.True()) + } + if attr := args.GetAttr("has_kev"); !attr.IsNull() { + req.HasKev = client.Bool(attr.True()) + } + if attr := args.GetAttr("has_cert_notes"); !attr.IsNull() { + req.HasCertNotes = client.Bool(attr.True()) + } + if attr := args.GetAttr("is_vulnerable"); !attr.IsNull() { + req.IsVulnerable = client.Bool(attr.True()) + } + if attr := args.GetAttr("keyword_exact_match"); !attr.IsNull() { + req.KeywordExactMatch = client.Bool(attr.True()) + } + if attr := args.GetAttr("no_rejected"); !attr.IsNull() { + req.NoRejected = client.Bool(attr.True()) + } + return req, nil +} diff --git a/internal/nistnvd/data_nist_nvd_cves_test.go b/internal/nistnvd/data_nist_nvd_cves_test.go new file mode 100644 index 00000000..aa02d625 --- /dev/null +++ b/internal/nistnvd/data_nist_nvd_cves_test.go @@ -0,0 +1,199 @@ +package nistnvd + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/zclconf/go-cty/cty" + + "github.com/blackstork-io/fabric/internal/nistnvd/client" + client_mocks "github.com/blackstork-io/fabric/mocks/internalpkg/nistnvd/client" + "github.com/blackstork-io/fabric/plugin" +) + +type CVESDataSourceTestSuite struct { + suite.Suite + schema *plugin.DataSource + ctx context.Context + cli *client_mocks.Client + storedApiKey *string +} + +func TestCVESDataSourceTestSuite(t *testing.T) { + suite.Run(t, new(CVESDataSourceTestSuite)) +} + +func (s *CVESDataSourceTestSuite) SetupSuite() { + s.schema = makeNistNvdCvesDataSource(func(apiKey *string) client.Client { + s.storedApiKey = apiKey + return s.cli + }) + s.ctx = context.Background() +} + +func (s *CVESDataSourceTestSuite) SetupTest() { + s.cli = &client_mocks.Client{} +} + +func (s *CVESDataSourceTestSuite) TearDownTest() { + s.cli.AssertExpectations(s.T()) +} + +func (s *CVESDataSourceTestSuite) TestSchema() { + s.Require().NotNil(s.schema) + s.NotNil(s.schema.Config) + s.NotNil(s.schema.Args) + s.NotNil(s.schema.DataFunc) +} + +func (s *CVESDataSourceTestSuite) TestLimit() { + s.cli.On("ListCVES", mock.Anything, &client.ListCVESReq{ + ResultsPerPage: 123, + StartIndex: 0, + }).Return(&client.ListCVESRes{ + ResultsPerPage: 123, + StartIndex: 0, + TotalResults: 1, + Vulnerabilities: []any{ + map[string]any{ + "id": "1", + }, + }, + }, nil) + res, diags := s.schema.DataFunc(s.ctx, &plugin.RetrieveDataParams{ + Config: cty.ObjectVal(map[string]cty.Value{ + "api_key": cty.StringVal("test_key"), + }), + Args: cty.ObjectVal(map[string]cty.Value{ + "last_mod_start_date": cty.NullVal(cty.String), + "last_mod_end_date": cty.NullVal(cty.String), + "pub_start_date": cty.NullVal(cty.String), + "pub_end_date": cty.NullVal(cty.String), + "cpe_name": cty.NullVal(cty.String), + "cve_id": cty.NullVal(cty.String), + "cvss_v3_metrics": cty.NullVal(cty.String), + "cvss_v3_severity": cty.NullVal(cty.String), + "cwe_id": cty.NullVal(cty.String), + "keyword_search": cty.NullVal(cty.String), + "virtual_match_string": cty.NullVal(cty.String), + "source_identifier": cty.NullVal(cty.String), + "has_cert_alerts": cty.NullVal(cty.String), + "has_kev": cty.NullVal(cty.Bool), + "has_cert_notes": cty.NullVal(cty.Bool), + "is_vulnerable": cty.NullVal(cty.Bool), + "keyword_exact_match": cty.NullVal(cty.Bool), + "no_rejected": cty.NullVal(cty.Bool), + "limit": cty.NumberIntVal(123), + }), + }) + s.Equal("test_key", *s.storedApiKey) + s.Len(diags, 0) + s.Equal(plugin.ListData{ + plugin.MapData{ + "id": plugin.StringData("1"), + }, + }, res) +} + +func (s *CVESDataSourceTestSuite) TestFull() { + s.cli.On("ListCVES", mock.Anything, &client.ListCVESReq{ + ResultsPerPage: 1, + StartIndex: 0, + LastModStartDate: client.String("2021-01-01T00:00:00Z"), + LastModEndDate: client.String("2021-01-02T00:00:00Z"), + PubStartDate: client.String("2021-01-03T00:00:00Z"), + PubEndDate: client.String("2021-01-04T00:00:00Z"), + CPEName: client.String("cpe:2.3:o:microsoft:windows_10:1607"), + CVEID: client.String("cve-2021-1234"), + CVSSV3Metrics: client.String("cvssv3"), + CVSSV3Severity: client.String("high"), + VirtualMatchString: client.String("virtual"), + CWEID: client.String("cwe-123"), + HasCertAlerts: client.Bool(true), + HasCertNotes: client.Bool(true), + HasKev: client.Bool(true), + IsVulnerable: client.Bool(true), + NoRejected: client.Bool(true), + KeywordSearch: client.String("keyword"), + KeywordExactMatch: client.Bool(true), + SourceIdentifier: client.String("source"), + }).Return(&client.ListCVESRes{ + ResultsPerPage: 1, + StartIndex: 0, + TotalResults: 2, + Vulnerabilities: []any{ + map[string]any{ + "id": "1", + }, + }, + }, nil) + s.cli.On("ListCVES", mock.Anything, &client.ListCVESReq{ + ResultsPerPage: 1, + StartIndex: 1, + LastModStartDate: client.String("2021-01-01T00:00:00Z"), + LastModEndDate: client.String("2021-01-02T00:00:00Z"), + PubStartDate: client.String("2021-01-03T00:00:00Z"), + PubEndDate: client.String("2021-01-04T00:00:00Z"), + CPEName: client.String("cpe:2.3:o:microsoft:windows_10:1607"), + CVEID: client.String("cve-2021-1234"), + VirtualMatchString: client.String("virtual"), + CVSSV3Metrics: client.String("cvssv3"), + CVSSV3Severity: client.String("high"), + CWEID: client.String("cwe-123"), + HasCertAlerts: client.Bool(true), + HasCertNotes: client.Bool(true), + HasKev: client.Bool(true), + IsVulnerable: client.Bool(true), + NoRejected: client.Bool(true), + KeywordSearch: client.String("keyword"), + KeywordExactMatch: client.Bool(true), + SourceIdentifier: client.String("source"), + }).Return(&client.ListCVESRes{ + ResultsPerPage: 1, + StartIndex: 1, + TotalResults: 2, + Vulnerabilities: []any{ + map[string]any{ + "id": "2", + }, + }, + }, nil) + res, diags := s.schema.DataFunc(s.ctx, &plugin.RetrieveDataParams{ + Config: cty.ObjectVal(map[string]cty.Value{ + "api_key": cty.StringVal("test_key"), + }), + Args: cty.ObjectVal(map[string]cty.Value{ + "last_mod_start_date": cty.StringVal("2021-01-01T00:00:00Z"), + "last_mod_end_date": cty.StringVal("2021-01-02T00:00:00Z"), + "pub_start_date": cty.StringVal("2021-01-03T00:00:00Z"), + "pub_end_date": cty.StringVal("2021-01-04T00:00:00Z"), + "cpe_name": cty.StringVal("cpe:2.3:o:microsoft:windows_10:1607"), + "cve_id": cty.StringVal("cve-2021-1234"), + "cvss_v3_metrics": cty.StringVal("cvssv3"), + "cvss_v3_severity": cty.StringVal("high"), + "cwe_id": cty.StringVal("cwe-123"), + "keyword_search": cty.StringVal("keyword"), + "virtual_match_string": cty.StringVal("virtual"), + "source_identifier": cty.StringVal("source"), + "has_cert_alerts": cty.BoolVal(true), + "has_kev": cty.BoolVal(true), + "has_cert_notes": cty.BoolVal(true), + "is_vulnerable": cty.BoolVal(true), + "keyword_exact_match": cty.BoolVal(true), + "no_rejected": cty.BoolVal(true), + "limit": cty.NumberIntVal(1), + }), + }) + s.Equal("test_key", *s.storedApiKey) + s.Len(diags, 0) + s.Equal(plugin.ListData{ + plugin.MapData{ + "id": plugin.StringData("1"), + }, + plugin.MapData{ + "id": plugin.StringData("2"), + }, + }, res) +} diff --git a/internal/nistnvd/plugin.go b/internal/nistnvd/plugin.go new file mode 100644 index 00000000..9ce0255c --- /dev/null +++ b/internal/nistnvd/plugin.go @@ -0,0 +1,20 @@ +package nistnvd + +import ( + "github.com/blackstork-io/fabric/internal/nistnvd/client" + "github.com/blackstork-io/fabric/plugin" +) + +type ClientLoadFn func(apiKey *string) client.Client + +var DefaultClientLoader ClientLoadFn = client.New + +func Plugin(version string, loader ClientLoadFn) *plugin.Schema { + return &plugin.Schema{ + Name: "blackstork/nist_nvd", + Version: version, + DataSources: plugin.DataSources{ + "nist_nvd_cves": makeNistNvdCvesDataSource(loader), + }, + } +} diff --git a/internal/nistnvd/plugin_test.go b/internal/nistnvd/plugin_test.go new file mode 100644 index 00000000..fc7d6de8 --- /dev/null +++ b/internal/nistnvd/plugin_test.go @@ -0,0 +1,14 @@ +package nistnvd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPlugin_Schema(t *testing.T) { + schema := Plugin("1.2.3", nil) + assert.Equal(t, "blackstork/nist_nvd", schema.Name) + assert.Equal(t, "1.2.3", schema.Version) + assert.NotNil(t, schema.DataSources["nist_nvd_cves"]) +} diff --git a/internal/plugin_validity_test.go b/internal/plugin_validity_test.go index 0452f462..b4549e7f 100644 --- a/internal/plugin_validity_test.go +++ b/internal/plugin_validity_test.go @@ -13,6 +13,7 @@ import ( "github.com/blackstork-io/fabric/internal/github" "github.com/blackstork-io/fabric/internal/graphql" "github.com/blackstork-io/fabric/internal/hackerone" + "github.com/blackstork-io/fabric/internal/nistnvd" "github.com/blackstork-io/fabric/internal/openai" "github.com/blackstork-io/fabric/internal/opencti" "github.com/blackstork-io/fabric/internal/postgresql" @@ -41,6 +42,7 @@ func TestAllPluginSchemaValidity(t *testing.T) { virustotal.Plugin(ver, nil), stixview.Plugin(ver), splunk.Plugin(ver, nil), + nistnvd.Plugin(ver, nil), } for _, p := range plugins { p := p diff --git a/mocks/internalpkg/nistnvd/client/client.go b/mocks/internalpkg/nistnvd/client/client.go new file mode 100644 index 00000000..25637cde --- /dev/null +++ b/mocks/internalpkg/nistnvd/client/client.go @@ -0,0 +1,97 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package client_mocks + +import ( + context "context" + + client "github.com/blackstork-io/fabric/internal/nistnvd/client" + + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +type Client_Expecter struct { + mock *mock.Mock +} + +func (_m *Client) EXPECT() *Client_Expecter { + return &Client_Expecter{mock: &_m.Mock} +} + +// ListCVES provides a mock function with given fields: ctx, req +func (_m *Client) ListCVES(ctx context.Context, req *client.ListCVESReq) (*client.ListCVESRes, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for ListCVES") + } + + var r0 *client.ListCVESRes + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *client.ListCVESReq) (*client.ListCVESRes, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, *client.ListCVESReq) *client.ListCVESRes); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.ListCVESRes) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *client.ListCVESReq) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_ListCVES_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListCVES' +type Client_ListCVES_Call struct { + *mock.Call +} + +// ListCVES is a helper method to define mock.On call +// - ctx context.Context +// - req *client.ListCVESReq +func (_e *Client_Expecter) ListCVES(ctx interface{}, req interface{}) *Client_ListCVES_Call { + return &Client_ListCVES_Call{Call: _e.mock.On("ListCVES", ctx, req)} +} + +func (_c *Client_ListCVES_Call) Run(run func(ctx context.Context, req *client.ListCVESReq)) *Client_ListCVES_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*client.ListCVESReq)) + }) + return _c +} + +func (_c *Client_ListCVES_Call) Return(_a0 *client.ListCVESRes, _a1 error) *Client_ListCVES_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_ListCVES_Call) RunAndReturn(run func(context.Context, *client.ListCVESReq) (*client.ListCVESRes, error)) *Client_ListCVES_Call { + _c.Call.Return(run) + return _c +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClient(t interface { + mock.TestingT + Cleanup(func()) +}) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/plugin/resolver/source.go b/plugin/resolver/source.go index 41c309ed..3ed1b8ae 100644 --- a/plugin/resolver/source.go +++ b/plugin/resolver/source.go @@ -45,7 +45,7 @@ func (source *sourceChain) Lookup(ctx context.Context, name Name) ([]Version, er var matches []Version for _, s := range source.sources { found, err := s.Lookup(ctx, name) - if err != nil { + if err != nil && err != ErrPluginNotFound { return nil, err } matches = append(matches, found...) diff --git a/plugin/resolver/source_remote.go b/plugin/resolver/source_remote.go index d0df24fe..63c27765 100644 --- a/plugin/resolver/source_remote.go +++ b/plugin/resolver/source_remote.go @@ -70,7 +70,11 @@ func (err regError) Error() string { func (source RemoteSource) Lookup(ctx context.Context, name Name) ([]Version, error) { versions, err := source.fetchVersions(ctx, name) if err != nil { - return nil, fmt.Errorf("failed to lookup plugin versions in the registry: %w", err) + if rerr, ok := err.(regError); ok && rerr.Code == "not_found" { + return nil, ErrPluginNotFound + } else { + return nil, fmt.Errorf("failed to lookup plugin versions in the registry: %w", err) + } } var matches []Version for _, version := range versions { diff --git a/plugin/resolver/source_remote_test.go b/plugin/resolver/source_remote_test.go index 1218ca36..e9ce7f3d 100644 --- a/plugin/resolver/source_remote_test.go +++ b/plugin/resolver/source_remote_test.go @@ -76,7 +76,7 @@ func TestRemoteSourceLookupError(t *testing.T) { BaseURL: srv.URL, } versions, err := source.Lookup(context.Background(), Name{"blackstork", "sqlite"}) - assert.EqualError(t, err, "failed to lookup plugin versions in the registry: [code=not_found]: plugin not found") + assert.EqualError(t, err, "plugin not found") assert.Nil(t, versions) } diff --git a/tools/docgen/content-provider.md.gotempl b/tools/docgen/content-provider.md.gotempl index 751c3fa1..c76875b5 100644 --- a/tools/docgen/content-provider.md.gotempl +++ b/tools/docgen/content-provider.md.gotempl @@ -5,7 +5,11 @@ plugin: description: "" tags: [] version: "{{ .plugin.Version }}" + {{- if eq .plugin_shortname "nist_nvd" }} + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/nistnvd/" + {{- else }} source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/{{ .plugin_shortname }}/" + {{- end }} resource: type: content-provider type: docs diff --git a/tools/docgen/data-source.md.gotempl b/tools/docgen/data-source.md.gotempl index 41c8cee6..448a3521 100644 --- a/tools/docgen/data-source.md.gotempl +++ b/tools/docgen/data-source.md.gotempl @@ -5,7 +5,11 @@ plugin: description: "" tags: [] version: "{{ .plugin.Version }}" + {{- if eq .plugin_shortname "nist_nvd" }} + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/nistnvd/" + {{- else }} source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/{{ .plugin_shortname }}/" + {{- end }} resource: type: data-source type: docs diff --git a/tools/docgen/main.go b/tools/docgen/main.go index fbdc3e6e..cfdc46aa 100644 --- a/tools/docgen/main.go +++ b/tools/docgen/main.go @@ -18,6 +18,7 @@ import ( "github.com/blackstork-io/fabric/internal/github" "github.com/blackstork-io/fabric/internal/graphql" "github.com/blackstork-io/fabric/internal/hackerone" + "github.com/blackstork-io/fabric/internal/nistnvd" "github.com/blackstork-io/fabric/internal/openai" "github.com/blackstork-io/fabric/internal/opencti" "github.com/blackstork-io/fabric/internal/postgresql" @@ -214,6 +215,7 @@ func main() { virustotal.Plugin(version, nil), splunk.Plugin(version, nil), stixview.Plugin(version), + nistnvd.Plugin(version, nil), } // generate markdown for each plugin for _, p := range plugins { diff --git a/tools/docgen/plugin.md.gotempl b/tools/docgen/plugin.md.gotempl index 47dda435..de037270 100644 --- a/tools/docgen/plugin.md.gotempl +++ b/tools/docgen/plugin.md.gotempl @@ -6,7 +6,11 @@ plugin: description: "" tags: [] version: "{{ .Version }}" + {{- if eq (shortname .Name) "nist_nvd" }} + source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/nistnvd/" + {{- else }} source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/{{ shortname .Name }}/" + {{- end }} type: docs ---