├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── new-plugin-request.md ├── diagram.png ├── fabric.svg ├── styles │ ├── .vale-config │ │ ├── 1-Hugo.ini │ │ └── 2-Hugo.ini │ └── config │ │ └── vocabularies │ │ └── fabric-vocab │ │ └── accept.txt └── workflows │ ├── deps.yml │ ├── docs.yml │ ├── install_tools │ └── action.yml │ ├── main.yml │ ├── main_common.yml │ ├── main_full.yml │ ├── main_partial.yml │ └── release.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser-dev.yaml ├── .goreleaser.yaml ├── .markdownlint.yaml ├── .mockery.yaml ├── .vale.ini ├── Dockerfile ├── LICENSE ├── README.md ├── buf.gen.yaml ├── buf.work.yaml ├── cmd ├── data.go ├── fabctx │ ├── eval_ctx.go │ ├── eval_ctx_test.go │ └── fabctx.go ├── install.go ├── internal │ ├── multilog │ │ └── handler.go │ └── telemetry │ │ └── telemetry.go ├── lint.go ├── render.go ├── root.go ├── usage.go ├── validations.go └── version.go ├── codegen ├── format.sh ├── gen.sh ├── gen_code.sh ├── gen_docs.sh ├── lint_protobuf.sh ├── rm_gen.sh ├── setup.sh └── utils.sh ├── docs ├── _index.md ├── cli.md ├── install.md ├── language │ ├── _index.md │ ├── configs.md │ ├── content-blocks.md │ ├── context.md │ ├── data-blocks.md │ ├── documents.md │ ├── dynamic-blocks.md │ ├── hcl.md │ ├── publish-blocks.md │ ├── references.md │ ├── section-blocks.md │ ├── syntax.md │ └── vscode-fabric-screenshot.png ├── plugins │ ├── _index.md │ ├── atlassian │ │ ├── _index.md │ │ └── data-sources │ │ │ └── jira_issues.md │ ├── builtin │ │ ├── _index.md │ │ ├── content-providers │ │ │ ├── blockquote.md │ │ │ ├── code.md │ │ │ ├── frontmatter.md │ │ │ ├── image.md │ │ │ ├── list.md │ │ │ ├── sleep.md │ │ │ ├── table.md │ │ │ ├── text.md │ │ │ ├── title.md │ │ │ └── toc.md │ │ ├── data-sources │ │ │ ├── csv.md │ │ │ ├── http.md │ │ │ ├── json.md │ │ │ ├── rss.md │ │ │ ├── sleep.md │ │ │ ├── txt.md │ │ │ └── yaml.md │ │ └── publishers │ │ │ ├── hub.md │ │ │ └── local_file.md │ ├── content-providers.md │ ├── crowdstrike │ │ ├── _index.md │ │ └── data-sources │ │ │ ├── falcon_cspm_ioms.md │ │ │ ├── falcon_detection_details.md │ │ │ ├── falcon_discover_host_details.md │ │ │ ├── falcon_intel_indicators.md │ │ │ └── falcon_vulnerabilities.md │ ├── data-sources.md │ ├── elastic │ │ ├── _index.md │ │ └── data-sources │ │ │ ├── elastic_security_cases.md │ │ │ └── elasticsearch.md │ ├── github │ │ ├── _index.md │ │ ├── data-sources │ │ │ └── github_issues.md │ │ └── publishers │ │ │ └── github_gist.md │ ├── graphql │ │ ├── _index.md │ │ └── data-sources │ │ │ └── graphql.md │ ├── hackerone │ │ ├── _index.md │ │ └── data-sources │ │ │ └── hackerone_reports.md │ ├── iris │ │ ├── _index.md │ │ └── data-sources │ │ │ ├── iris_alerts.md │ │ │ └── iris_cases.md │ ├── microsoft │ │ ├── _index.md │ │ ├── content-providers │ │ │ └── azure_openai_text.md │ │ └── data-sources │ │ │ ├── microsoft_graph.md │ │ │ ├── microsoft_security.md │ │ │ ├── microsoft_security_query.md │ │ │ └── microsoft_sentinel_incidents.md │ ├── misp │ │ ├── _index.md │ │ ├── data-sources │ │ │ └── misp_events.md │ │ └── publishers │ │ │ └── misp_event_reports.md │ ├── nist_nvd │ │ ├── _index.md │ │ └── data-sources │ │ │ └── nist_nvd_cves.md │ ├── openai │ │ ├── _index.md │ │ └── content-providers │ │ │ └── openai_text.md │ ├── opencti │ │ ├── _index.md │ │ └── data-sources │ │ │ └── opencti.md │ ├── plugins.json │ ├── postgresql │ │ ├── _index.md │ │ └── data-sources │ │ │ └── postgresql.md │ ├── publishers.md │ ├── snyk │ │ ├── _index.md │ │ └── data-sources │ │ │ └── snyk_issues.md │ ├── splunk │ │ ├── _index.md │ │ └── data-sources │ │ │ └── splunk_search.md │ ├── sqlite │ │ ├── _index.md │ │ └── data-sources │ │ │ └── sqlite.md │ ├── stixview │ │ ├── _index.md │ │ └── content-providers │ │ │ └── stixview.md │ ├── terraform │ │ ├── _index.md │ │ └── data-sources │ │ │ └── terraform_state_local.md │ └── virustotal │ │ ├── _index.md │ │ └── data-sources │ │ └── virustotal_api_usage.md └── tutorial.md ├── engine ├── depends_on_ref_test.go ├── dynamic_test.go ├── engine.go ├── engine_test.go ├── engine_vars_test.go ├── fuzz_test.go ├── helpers_test.go ├── is_included_test.go ├── is_included_vars_test.go ├── options.go ├── tags_test.go └── testdata │ └── a.json ├── eval ├── content.go ├── document.go ├── dynamic.go ├── evaluator.go ├── plugin_action.go ├── plugin_content_action.go ├── plugin_data_action.go ├── plugin_publish_action.go ├── plugins.go ├── section.go └── vars.go ├── examples ├── plugins │ ├── README.md │ └── basic │ │ ├── cmd │ │ └── main.go │ │ ├── content_greeting.go │ │ ├── data_random_numbers.go │ │ └── plugin.go └── templates │ ├── async │ └── example.fabric │ ├── atlassian │ └── example.fabric │ ├── azureopenai │ └── example.fabric │ ├── basic_hello │ └── hello.fabric │ ├── crowdstrike │ ├── data_falcon_cspm_ioms.fabric │ ├── data_falcon_detection_details.fabric │ ├── data_falcon_discover_host_details.fabric │ ├── data_falcon_intel_indicators.fabric │ └── data_falcon_vulnerabilities.fabric │ ├── frontmatter │ └── example.fabric │ ├── github │ └── publish_gist.fabric │ ├── hackerone │ └── example.fabric │ ├── hub │ └── example.fabric │ ├── iris │ └── example.fabric │ ├── microsoft │ └── graph_data_source.fabric │ ├── misp │ ├── misp_event_reports.fabric │ └── misp_events.fabric │ ├── nist_nvd │ └── example.fabric │ ├── openai │ ├── data.csv │ └── example.fabric │ ├── publish │ └── example.fabric │ ├── sections │ └── example.fabric │ ├── stixview │ └── example.fabric │ └── virustotal │ └── example.fabric ├── gen.go ├── go.mod ├── go.sum ├── internal ├── atlassian │ ├── client │ │ ├── client.go │ │ ├── client_test.go │ │ └── dto.go │ ├── cmd │ │ └── main.go │ ├── data_jira_issues.go │ ├── data_jira_issues_test.go │ ├── plugin.go │ └── plugin_test.go ├── builtin │ ├── content_blockquote.go │ ├── content_blockquote_test.go │ ├── content_code.go │ ├── content_code_test.go │ ├── content_frontmatter.go │ ├── content_frontmatter_test.go │ ├── content_helpers.go │ ├── content_image.go │ ├── content_image_test.go │ ├── content_list.go │ ├── content_list_test.go │ ├── content_sleep.go │ ├── content_sleep_test.go │ ├── content_table.go │ ├── content_table_test.go │ ├── content_text.go │ ├── content_text_test.go │ ├── content_title.go │ ├── content_title_test.go │ ├── content_toc.go │ ├── content_toc_test.go │ ├── data_csv.go │ ├── data_csv_test.go │ ├── data_http.go │ ├── data_http_test.go │ ├── data_json.go │ ├── data_json_test.go │ ├── data_rss.go │ ├── data_rss_test.go │ ├── data_sleep.go │ ├── data_sleep_test.go │ ├── data_txt.go │ ├── data_txt_test.go │ ├── data_yaml.go │ ├── data_yaml_test.go │ ├── hubapi │ │ ├── client.go │ │ ├── client_test.go │ │ └── dto.go │ ├── plugin.go │ ├── plugin_test.go │ ├── publish_hub.go │ ├── publish_hub_test.go │ ├── publish_local_file.go │ ├── publish_local_file_test.go │ ├── testdata │ │ ├── csv │ │ │ ├── comma.csv │ │ │ ├── empty.csv │ │ │ ├── invalid.csv │ │ │ └── semicolon.csv │ │ ├── json │ │ │ ├── a.json │ │ │ ├── dir │ │ │ │ ├── b.json │ │ │ │ └── c.json │ │ │ └── invalid.txt │ │ ├── rss │ │ │ ├── basic.atom │ │ │ └── basic.rss │ │ ├── txt │ │ │ ├── data.txt │ │ │ └── empty.txt │ │ └── yaml │ │ │ ├── a.yaml │ │ │ ├── dir │ │ │ ├── b.yaml │ │ │ └── c.yaml │ │ │ └── invalid.txt │ └── utils │ │ └── parsers.go ├── crowdstrike │ ├── cmd │ │ └── main.go │ ├── data_falcon_cspm_ioms.go │ ├── data_falcon_cspm_ioms_test.go │ ├── data_falcon_detection_details.go │ ├── data_falcon_detection_details_test.go │ ├── data_falcon_discover_host_details.go │ ├── data_falcon_discover_host_details_test.go │ ├── data_falcon_intel_indicators.go │ ├── data_falcon_intel_indicators_test.go │ ├── data_falcon_vulnerabilities.go │ ├── data_falcon_vulnerabilities_test.go │ ├── plugin.go │ └── plugin_test.go ├── elastic │ ├── cmd │ │ └── main.go │ ├── data_elastic_security_cases.go │ ├── data_elastic_security_cases_test.go │ ├── data_elasticsearch.go │ ├── data_elasticsearch_test.go │ ├── kbclient │ │ ├── client.go │ │ └── client_test.go │ ├── plugin.go │ ├── plugin_test.go │ └── testdata │ │ └── data.json ├── github │ ├── cmd │ │ └── main.go │ ├── data_github_issues.go │ ├── data_github_issues_test.go │ ├── plugin.go │ ├── plugin_test.go │ ├── publish_github_gist.go │ └── publish_github_gist_test.go ├── graphql │ ├── cmd │ │ └── main.go │ ├── data_graphql.go │ ├── data_graphql_test.go │ ├── plugin.go │ └── plugin_test.go ├── hackerone │ ├── client │ │ ├── client.go │ │ └── client_test.go │ ├── cmd │ │ └── main.go │ ├── data_hackerone_reports.go │ ├── data_hackerone_reports_test.go │ ├── plugin.go │ └── plugin_test.go ├── iris │ ├── client │ │ ├── client.go │ │ ├── client_test.go │ │ └── dto.go │ ├── cmd │ │ └── main.go │ ├── data_iris_alerts.go │ ├── data_iris_alerts_test.go │ ├── data_iris_cases.go │ ├── data_iris_cases_test.go │ ├── plugin.go │ └── plugin_test.go ├── microsoft │ ├── client │ │ ├── azure_client.go │ │ ├── azure_client_test.go │ │ ├── graph_client.go │ │ ├── microsoftonline_client.go │ │ └── security_client.go │ ├── cmd │ │ └── main.go │ ├── content_azure_openai_text.go │ ├── content_azure_openai_text_test.go │ ├── data_microsoft_graph.go │ ├── data_microsoft_graph_test.go │ ├── data_microsoft_security.go │ ├── data_microsoft_security_query.go │ ├── data_microsoft_security_query_test.go │ ├── data_microsoft_security_test.go │ ├── data_microsoft_sentinel_incidents.go │ ├── data_microsoft_sentinel_incidents_test.go │ ├── plugin.go │ └── plugin_test.go ├── misp │ ├── client │ │ ├── misp_client.go │ │ └── misp_models.go │ ├── cmd │ │ └── main.go │ ├── data_misp_events.go │ ├── data_misp_events_test.go │ ├── plugin.go │ ├── plugin_test.go │ ├── publish_misp_event_reports.go │ └── publish_misp_event_reports_test.go ├── nistnvd │ ├── client │ │ ├── client.go │ │ └── client_test.go │ ├── cmd │ │ └── main.go │ ├── data_nist_nvd_cves.go │ ├── data_nist_nvd_cves_test.go │ ├── plugin.go │ └── plugin_test.go ├── openai │ ├── client │ │ ├── client.go │ │ ├── client_test.go │ │ ├── options.go │ │ └── types.go │ ├── cmd │ │ └── main.go │ ├── content_openai_text.go │ ├── content_openai_text_test.go │ ├── plugin.go │ └── plugin_test.go ├── opencti │ ├── cmd │ │ └── main.go │ ├── data_opencti.go │ ├── data_opencti_test.go │ ├── opencti.graphql │ ├── plugin.go │ └── plugin_test.go ├── plugin_validity_test.go ├── postgresql │ ├── cmd │ │ └── main.go │ ├── data_postgresql.go │ ├── data_postgresql_test.go │ ├── plugin.go │ ├── plugin_test.go │ └── testdata │ │ └── data.sql ├── snyk │ ├── client │ │ ├── client.go │ │ └── client_test.go │ ├── cmd │ │ └── main.go │ ├── data_snyk_issues.go │ ├── data_snyk_issues_test.go │ ├── plugin.go │ └── plugin_test.go ├── splunk │ ├── client │ │ ├── client.go │ │ ├── client_test.go │ │ └── dto.go │ ├── cmd │ │ └── main.go │ ├── data_splunk_search.go │ ├── data_splunk_search_test.go │ ├── plugin.go │ └── plugin_test.go ├── sqlite │ ├── cmd │ │ └── main.go │ ├── data_sqlite.go │ ├── data_sqlite_test.go │ ├── helpers_test.go │ ├── plugin.go │ └── plugin_test.go ├── stixview │ ├── cmd │ │ └── main.go │ ├── content_stixview.go │ ├── content_stixview_test.go │ ├── plugin.go │ ├── plugin_test.go │ └── stixview.gohtml ├── terraform │ ├── cmd │ │ └── main.go │ ├── data_terraform_state_local.go │ ├── data_terraform_state_local_test.go │ ├── plugin.go │ ├── plugin_test.go │ └── testdata │ │ └── terraform.tfstate └── virustotal │ ├── client │ ├── client.go │ ├── client_test.go │ └── dto.go │ ├── cmd │ └── main.go │ ├── data_virustotal_api_usage.go │ ├── data_virustotal_api_usage_test.go │ ├── plugin.go │ └── plugin_test.go ├── justfile ├── main.go ├── mocks ├── internalpkg │ ├── atlassian │ │ └── client │ │ │ └── client.go │ ├── builtin │ │ └── hubapi │ │ │ └── client.go │ ├── crowdstrike │ │ ├── client.go │ │ ├── cspm_registration_client.go │ │ ├── detects_client.go │ │ ├── discover_client.go │ │ ├── intel_client.go │ │ └── spot_vulnerabilities_client.go │ ├── elastic │ │ └── kbclient │ │ │ └── client.go │ ├── github │ │ ├── client.go │ │ ├── gist_client.go │ │ └── issues_client.go │ ├── hackerone │ │ └── client │ │ │ └── client.go │ ├── iris │ │ └── client │ │ │ └── client.go │ ├── microsoft │ │ ├── azure_client.go │ │ ├── azure_open_ai_client.go │ │ ├── microsoft_graph_client.go │ │ └── microsoft_security_client.go │ ├── misp │ │ └── client.go │ ├── nistnvd │ │ └── client │ │ │ └── client.go │ ├── openai │ │ └── client │ │ │ └── client.go │ ├── snyk │ │ └── client │ │ │ └── client.go │ ├── splunk │ │ └── client │ │ │ └── client.go │ └── virustotal │ │ └── client │ │ └── client.go └── parser │ └── definitions │ └── fabric_block.go ├── parser ├── defined_blocks.go ├── defined_blocks_test.go ├── definitions │ ├── config.go │ ├── config_empty.go │ ├── config_ptr.go │ ├── definitions.go │ ├── document.go │ ├── dynamic.go │ ├── global_config.go │ ├── meta.go │ ├── parsed_document.go │ ├── parsed_plugin.go │ ├── plugin.go │ ├── section.go │ ├── validators.go │ └── vars.go ├── evaluation │ ├── block_invocation.go │ └── evaluation.go ├── parse_document.go ├── parse_dynamic.go ├── parse_plugin.go ├── parse_section.go ├── parse_title.go ├── parse_vars.go ├── parser.go ├── parser_test.go └── traversal.go ├── pkg ├── circularRefDetector │ ├── circularRefs.go │ └── circularRefs_test.go ├── diagnostics │ ├── diagnostics.go │ ├── diagtest │ │ ├── diagtest.go │ │ └── diagtest_test.go │ ├── extra.go │ ├── gojq.go │ ├── path.go │ ├── printer.go │ ├── refine.go │ ├── repeated.go │ └── traceback.go ├── encapsulator │ ├── codec.go │ ├── decoder.go │ ├── encapsulator_test.go │ ├── encoder.go │ ├── typed_capsule_ops.go │ ├── typed_capusle_opts_test.go │ └── util.go ├── jsontools │ └── jsontools.go ├── parexec │ ├── limiter.go │ ├── limiter_test.go │ ├── parexec.go │ ├── parexecRace_test.go │ └── parexec_test.go ├── sloghclog │ ├── sloghclog.go │ └── sloghclog_test.go └── utils │ ├── golang.go │ ├── hcl.go │ ├── maps.go │ ├── maps_test.go │ ├── slices.go │ ├── slices_test.go │ ├── slogutil │ └── slogutil.go │ ├── string.go │ └── string_test.go ├── plugin ├── ast │ ├── astsrc │ │ └── astsrc.go │ ├── builder.go │ ├── nodes │ │ ├── content_node.go │ │ ├── custom_nodes.go │ │ └── metadata.go │ └── v1 │ │ ├── ast.pb.go │ │ ├── ast_decoder.go │ │ ├── ast_encoder.go │ │ ├── ast_test.go │ │ ├── bubble_error.go │ │ ├── enum_codec.go │ │ ├── interfaces.go │ │ └── testdata │ │ └── fuzz │ │ └── FuzzEncoder │ │ └── 6b5e47038908d6af ├── content.go ├── content_provider.go ├── data_source.go ├── dataspec │ ├── attr.go │ ├── attr_spec.go │ ├── attr_spec_doc_comment.gotmpl │ ├── attr_spec_test.go │ ├── block.go │ ├── block_spec.go │ ├── constraint │ │ ├── constraint.go │ │ └── constraint_test.go │ ├── dataspec.go │ ├── decode.go │ ├── deferred │ │ ├── deferred_eval.go │ │ └── jq.go │ ├── genutil.go │ └── root_spec.go ├── errors.go ├── logging.go ├── pluginapi │ └── v1 │ │ ├── client.go │ │ ├── content.pb.go │ │ ├── content_decoder.go │ │ ├── content_encoder.go │ │ ├── cty.pb.go │ │ ├── cty_codec_test.go │ │ ├── cty_decoder.go │ │ ├── cty_encoder.go │ │ ├── data.pb.go │ │ ├── data_decoder.go │ │ ├── data_encoder.go │ │ ├── dataspec.pb.go │ │ ├── dataspec_decoder.go │ │ ├── dataspec_encoder.go │ │ ├── hcl.pb.go │ │ ├── hcl_decoder.go │ │ ├── hcl_encoder.go │ │ ├── is_not_plugin.go │ │ ├── is_plugin.go │ │ ├── plugin.go │ │ ├── plugin.pb.go │ │ ├── plugin_grpc.pb.go │ │ ├── schema.pb.go │ │ ├── schema_decoder.go │ │ ├── schema_encoder.go │ │ └── server.go ├── plugindata │ ├── data.go │ └── data_encapsulated.go ├── plugintest │ ├── blocktest.go │ └── plugintest.go ├── publisher.go ├── resolver │ ├── checksum.go │ ├── checksum_test.go │ ├── lockfile.go │ ├── lockfile_test.go │ ├── mock_source_test.go │ ├── name.go │ ├── name_test.go │ ├── options.go │ ├── resolver.go │ ├── resolver_test.go │ ├── source.go │ ├── source_local.go │ ├── source_local_test.go │ ├── source_remote.go │ ├── source_remote_test.go │ ├── version.go │ └── version_test.go ├── runner │ ├── loader.go │ └── runner.go ├── schema.go └── tracing.go ├── print ├── htmlprint │ ├── document.gotempl │ └── printer.go ├── logging.go ├── mdprint │ └── printer.go ├── pdfprint │ └── printer.go ├── printer.go └── tracing.go ├── proto ├── ast │ └── v1 │ │ └── ast.proto ├── buf.yaml └── pluginapi │ └── v1 │ ├── content.proto │ ├── cty.proto │ ├── data.proto │ ├── dataspec.proto │ ├── hcl.proto │ ├── plugin.proto │ └── schema.proto └── tools ├── docgen ├── content-provider.md.gotempl ├── data-source.md.gotempl ├── main.go ├── plugin.md.gotempl └── publisher.md.gotempl └── pluginmeta ├── main.go ├── metadata.go └── releaser_config.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.fabric linguist-language=hcl 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | Provide a clear and concise description of the bug you are experiencing. 12 | 13 | ### Environment 14 | Fabric version: [Specify the version of `fabric` CLI where the bug was observed] 15 | Operating System: [Specify the operating system and version] 16 | Terminal/Shell: [Specify the terminal or shell environment] 17 | 18 | ### Steps to Reproduce 19 | Clearly outline the steps to reproduce the bug. Include any relevant commands, inputs, or configurations that lead to the issue. If possible, provide a minimal code example or a step-by-step guide. 20 | 21 | 1. [Step 1] 22 | 2. [Step 2] 23 | 3. [Step 3] 24 | 4. ... 25 | 26 | ### Expected Behavior 27 | Describe what you expected to happen when performing the steps outlined above. This will help us understand the intended functionality and identify deviations from it. 28 | 29 | ### Actual Behavior 30 | Describe what actually happened when performing the steps outlined above. Include any error messages or unexpected outcomes that occurred during the process. 31 | 32 | ### Additional Context 33 | Include any additional context that might be helpful in resolving the bug. This could include screenshots, configurations, or any recent changes made to the environment. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-plugin-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New plugin request 3 | about: Create a request for a new plugin 4 | title: '' 5 | labels: plugins 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | Please provide a brief description of the functionality you would like to see implemented in the new plugin. 12 | 13 | ### Use Case 14 | Describe a real-world scenario or use case where this new plugin would be beneficial. This will help us understand the practical value and potential impact of the proposed plugin. 15 | 16 | ### Requirements 17 | List any specific requirements or features you expect from the new plugin. For example, input/output formats, compatibility with certain systems, or integration with other tools. 18 | 19 | ### Additional Information 20 | Include any other relevant information, such as links to documentation, examples, or external resources that may help in understanding the requirements for the new plugin. 21 | -------------------------------------------------------------------------------- /.github/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstork-io/fabric/122a20956a140c0413d68c061e56e9825c199082/.github/diagram.png -------------------------------------------------------------------------------- /.github/styles/.vale-config/1-Hugo.ini: -------------------------------------------------------------------------------- 1 | [*.md] 2 | # Exclude `{{< ... >}}`, `{{% ... %}}`, [Who]({{< ... >}}) 3 | TokenIgnores = ({{[%<] .* [%>]}}.*?{{[%<] ?/.* [%>]}}), \ 4 | (\[.+\]\({{< .+ >}}\)), \ 5 | ({{[%<] .+ [%>]}}) 6 | 7 | # Exclude `{{< myshortcode `This is some HTML, ... >}}` 8 | BlockIgnores = (?sm)^({{[%<] [^{]*? [%>]}})\n$, \ 9 | (?s) *({{< highlight [^>]* ?>}}.*?{{< ?/ ?highlight >}}), \ 10 | ({{[%<] .+ [%>]}}) 11 | -------------------------------------------------------------------------------- /.github/styles/.vale-config/2-Hugo.ini: -------------------------------------------------------------------------------- 1 | [*.md] 2 | # Exclude `{{< ... >}}`, `{{% ... %}}`, [Who]({{< ... >}}) 3 | TokenIgnores = ({{[%<] .* [%>]}}.*?{{[%<] ?/.* [%>]}}), \ 4 | (\[.+\]\({{< .+ >}}\)), \ 5 | ({{[%<] .+ [%>]}}) 6 | 7 | # Exclude `{{< myshortcode `This is some HTML, ... >}}` 8 | BlockIgnores = (?sm)^({{[%<] [^{]*? [%>]}})\n$, \ 9 | (?s) *({{< highlight [^>]* ?>}}.*?{{< ?/ ?highlight >}}), \ 10 | ({{[%<] .+ [%>]}}) 11 | -------------------------------------------------------------------------------- /.github/styles/config/vocabularies/fabric-vocab/accept.txt: -------------------------------------------------------------------------------- 1 | Fabric 2 | GitHub 3 | boolean 4 | namespace 5 | Sonoma 6 | API 7 | LLM 8 | Splunk 9 | Cyber 10 | CTI 11 | ASCII 12 | stdout 13 | -------------------------------------------------------------------------------- /.github/workflows/deps.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/go-dependency-submission 2 | name: dependency-submission 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.22.x" 19 | - uses: actions/go-dependency-submission@v2 20 | with: 21 | go-mod-path: go.mod 22 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**.md' 9 | pull_request: 10 | paths: 11 | - '**.md' 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: docs-${{ github.head_ref || github.run_id }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | # https://github.com/igorshubovych/markdownlint-cli 20 | markdownlint-cli: 21 | if: github.event_name != 'pull_request' || !github.event.pull_request.draft 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: nosborn/github-action-markdown-cli@v3.3.0 26 | with: 27 | files: '**/*.md' 28 | config_file: .markdownlint.yaml 29 | dot: true 30 | 31 | # https://github.com/errata-ai/vale-action 32 | vale: 33 | if: github.event_name != 'pull_request' || !github.event.pull_request.draft 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: errata-ai/vale-action@reviewdog 38 | with: 39 | fail_on_error: true 40 | vale_flags: "--glob='**/*.md'" 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/install_tools/action.yml: -------------------------------------------------------------------------------- 1 | name: Install code generation tools 2 | 3 | description: | 4 | Installs code generation tools, linters, etc 5 | Expects go to be installed 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Get installed go version 11 | run: echo "GOVERSION=$(go env GOVERSION)" >> "$GITHUB_ENV" 12 | shell: bash 13 | - name: Restore cached codegen tools 14 | id: cache-tools 15 | uses: actions/cache/restore@v4 16 | with: 17 | key: codegen-tools-${{ runner.os }}-${{ env.GOVERSION }}-${{ hashFiles('./codegen/setup.sh') }} 18 | path: ~/go/bin/ 19 | - name: Setup tools 20 | if: steps.cache-tools.outputs.cache-hit != 'true' 21 | run: ./codegen/setup.sh 22 | shell: bash 23 | 24 | - name: Cache codegen tools 25 | if: steps.cache-tools.outputs.cache-hit != 'true' 26 | uses: actions/cache/save@v4 27 | with: 28 | key: ${{ steps.cache-tools.outputs.cache-primary-key }} 29 | path: ~/go/bin/ -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | # PR checks 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | types: 10 | - opened 11 | - synchronize 12 | - reopened 13 | - ready_for_review 14 | pull_request_review: 15 | types: [submitted] 16 | 17 | workflow_dispatch: 18 | 19 | concurrency: 20 | group: main-${{ github.head_ref || github.sha }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | partial_checks: 25 | if: github.event_name == 'pull_request' && !github.event.pull_request.draft 26 | uses: ./.github/workflows/main_partial.yml 27 | 28 | full_checks: 29 | if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_review' && github.event.review.state == 'approved' 30 | uses: ./.github/workflows/main_full.yml 31 | 32 | common_checks: 33 | if: github.event_name != 'pull_request' || !github.event.pull_request.draft 34 | 35 | permissions: 36 | # For golangci-lint 37 | contents: read 38 | pull-requests: read 39 | checks: write 40 | uses: ./.github/workflows/main_common.yml 41 | -------------------------------------------------------------------------------- /.github/workflows/main_full.yml: -------------------------------------------------------------------------------- 1 | name: Full PR checks 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | tests: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | go-version: ["stable", "oldstable"] 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - run: git config --global core.autocrlf input 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | # Run integration tests (testcontainers) on ubuntu 21 | - run: go test -race -v -timeout 2m ${{ !startsWith(matrix.os, 'ubuntu') && '-short' || '' }} ./... 22 | 23 | builds: 24 | strategy: 25 | matrix: 26 | go-version: ["stable", "oldstable"] 27 | os: [ubuntu-latest, macos-latest, windows-latest] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - run: git config --global core.autocrlf input 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-go@v5 33 | with: 34 | go-version: ${{ matrix.go-version }} 35 | - name: Build fabric and plugins 36 | uses: goreleaser/goreleaser-action@v6 37 | with: 38 | distribution: goreleaser 39 | version: '~> v2' 40 | args: build --snapshot --clean --single-target 41 | -------------------------------------------------------------------------------- /.github/workflows/main_partial.yml: -------------------------------------------------------------------------------- 1 | name: Partial PR checks 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-go@v5 12 | with: 13 | go-version: "stable" 14 | - run: go test -race -v -timeout 10s -short ./... 15 | 16 | builds: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: "stable" 23 | - name: Build fabric and plugins 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | distribution: goreleaser 27 | version: '~> v2' 28 | args: build --snapshot --clean --single-target 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: stable 23 | - name: Login to Docker Hub 24 | uses: docker/login-action@v3 25 | with: 26 | # TODO: setup creds 27 | username: "${{ secrets.DOCKERHUB_USERNAME }}" 28 | password: "${{ secrets.DOCKERHUB_TOKEN }}" 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v6 31 | with: 32 | distribution: goreleaser 33 | version: '~> v2' 34 | args: release --clean 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | CONTAINER_REPO_NAME: "blackstorkio/fabric" 38 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Editor directories 5 | .vscode/ 6 | # Binaries for programs and plugins 7 | bin/* 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | .DS_Store 14 | 15 | # Vale will install modules in `.vale/style` and those should not be in version control 16 | .github/styles/* 17 | !.github/styles/config 18 | 19 | # Test binary, built with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Dependency directories (remove the comment below to include it) 26 | vendor/ 27 | 28 | # Go workspace file 29 | go.work 30 | 31 | dist/ 32 | .tmp 33 | .fabric 34 | .fabric-lock.json 35 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 2 | default: true # enable all by default 3 | MD007: # unordered list indentation 4 | indent: 2 5 | MD013: false # do not validate line length 6 | MD014: false # allow $ before command output 7 | MD024: false # allow duplicate headings, for example, "Example" 8 | MD029: # ordered list prefix, use all ones 9 | style: "one" 10 | MD033: false # allow inline HTML 11 | MD041: false # the first line is not always H1 12 | MD025: false # because it thinks that is there is frontmatter, there should be no H1 13 | -------------------------------------------------------------------------------- /.vale.ini: -------------------------------------------------------------------------------- 1 | MinAlertLevel = suggestion 2 | Packages = Microsoft, Hugo 3 | StylesPath = .github/styles 4 | Vocab = fabric-vocab 5 | 6 | [*.md] 7 | BasedOnStyles = Vale, Microsoft 8 | 9 | # Style.Rule = {YES, NO, suggestion, warning, error} to 10 | 11 | # Disable a requirement of no spaces around dashes 12 | Microsoft.Dashes = NO 13 | 14 | # Allow CLI in the headings 15 | Microsoft.HeadingAcronyms = NO 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM alpine:3.19 4 | ENTRYPOINT [ "/fabric" ] 5 | CMD [ "--help" ] 6 | COPY fabric /fabric -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | go_package_prefix: 5 | default: github.com/blackstork-io/fabric/plugin 6 | plugins: 7 | - plugin: buf.build/protocolbuffers/go 8 | out: . 9 | opt: module=github.com/blackstork-io/fabric 10 | - plugin: buf.build/grpc/go 11 | out: . 12 | opt: module=github.com/blackstork-io/fabric -------------------------------------------------------------------------------- /buf.work.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | directories: 3 | - proto/ 4 | -------------------------------------------------------------------------------- /cmd/fabctx/eval_ctx_test.go: -------------------------------------------------------------------------------- 1 | package fabctx 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/zclconf/go-cty/cty" 10 | ) 11 | 12 | func Test_EnvVars(t *testing.T) { 13 | assert := assert.New(t) 14 | t.Setenv("TEST_KEY", "test_value") 15 | evalCtx := newEvalContext() 16 | env := evalCtx.Variables["env"] 17 | assert.NotNil(env) 18 | assert.True(cty.Map(cty.String).Equals(env.Type())) 19 | envMap := env.AsValueMap() 20 | assert.True(envMap["NON_EXISTENT_KEY"].IsNull()) 21 | assert.False(envMap["TEST_KEY"].IsNull()) 22 | assert.Equal("test_value", envMap["TEST_KEY"].AsString()) 23 | } 24 | 25 | func TestFromFileFunc(t *testing.T) { 26 | const fileContents = "test file contents" 27 | assert := assert.New(t) 28 | tmp := t.TempDir() 29 | tmpPath := path.Join(tmp, "test") 30 | os.WriteFile(tmpPath, []byte(fileContents), 0o600) 31 | val, err := fromFileFunc.Call([]cty.Value{cty.StringVal(tmpPath)}) 32 | assert.NoError(err) 33 | assert.Equal(fileContents, val.AsString()) 34 | } 35 | 36 | func TestFuncsPresent(t *testing.T) { 37 | assert := assert.New(t) 38 | evalCtx := newEvalContext() 39 | assert.Contains(evalCtx.Functions, "from_file") 40 | assert.Contains(evalCtx.Functions, "join") 41 | } 42 | -------------------------------------------------------------------------------- /cmd/install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/blackstork-io/fabric/engine" 9 | "github.com/blackstork-io/fabric/internal/builtin" 10 | "github.com/blackstork-io/fabric/pkg/diagnostics" 11 | ) 12 | 13 | var installUpgrade bool 14 | 15 | func init() { 16 | rootCmd.AddCommand(installCmd) 17 | installCmd.Flags().BoolVarP(&installUpgrade, "upgrade", "u", false, "Upgrade plugin versions") 18 | } 19 | 20 | var installCmd = &cobra.Command{ 21 | Use: "install", 22 | Short: "Install plugins", 23 | Long: "Install Fabric plugins", 24 | RunE: func(cmd *cobra.Command, args []string) (err error) { 25 | ctx := cmd.Context() 26 | var diags diagnostics.Diag 27 | eng := engine.New( 28 | engine.WithLogger(slog.Default()), 29 | engine.WithTracer(tracer), 30 | engine.WithBuiltIn(builtin.Plugin(version, slog.Default(), tracer)), 31 | ) 32 | defer func() { 33 | err = exitCommand(eng, cmd, diags) 34 | }() 35 | diag := eng.ParseDir(ctx, cliArgs.sourceDir) 36 | if diags.Extend(diag) { 37 | return 38 | } 39 | diag = eng.LoadPluginResolver(ctx, true) 40 | if diags.Extend(diag) { 41 | return 42 | } 43 | diag = eng.Install(ctx, installUpgrade) 44 | if diags.Extend(diag) { 45 | return 46 | } 47 | return 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /cmd/internal/multilog/handler.go: -------------------------------------------------------------------------------- 1 | package multilog 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | ) 8 | 9 | // Handler is a log handler that forwards log records to multiple handlers. 10 | // It is useful when you want to log to multiple destinations, e.g. console and opentelemetry. 11 | type Handler struct { 12 | Level slog.Level 13 | Handlers []slog.Handler 14 | } 15 | 16 | // Enabled returns true if the log level is enabled. 17 | func (multi Handler) Enabled(ctx context.Context, level slog.Level) bool { 18 | return multi.Level <= level 19 | } 20 | 21 | // Handle forwards the log record to all handlers. 22 | func (multi Handler) Handle(ctx context.Context, record slog.Record) error { 23 | var errs []error 24 | for _, handler := range multi.Handlers { 25 | err := handler.Handle(ctx, record) 26 | if err != nil { 27 | errs = append(errs, err) 28 | } 29 | } 30 | return errors.Join(errs...) 31 | } 32 | 33 | // WithAttrs returns a new Handler with the given attributes. 34 | func (multi Handler) WithAttrs(attrs []slog.Attr) slog.Handler { 35 | handlers := make([]slog.Handler, len(multi.Handlers)) 36 | for i, handler := range multi.Handlers { 37 | handlers[i] = handler.WithAttrs(attrs) 38 | } 39 | multi.Handlers = handlers 40 | return multi 41 | } 42 | 43 | // WithGroup returns a new Handler with the given group name. 44 | func (multi Handler) WithGroup(name string) slog.Handler { 45 | handlers := make([]slog.Handler, len(multi.Handlers)) 46 | for i, handler := range multi.Handlers { 47 | handlers[i] = handler.WithGroup(name) 48 | } 49 | multi.Handlers = handlers 50 | return multi 51 | } 52 | -------------------------------------------------------------------------------- /cmd/lint.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/blackstork-io/fabric/engine" 9 | "github.com/blackstork-io/fabric/internal/builtin" 10 | "github.com/blackstork-io/fabric/pkg/diagnostics" 11 | ) 12 | 13 | var fullLint bool 14 | 15 | func init() { 16 | lintCmd.Flags().BoolVar(&fullLint, "full", false, "Lint plugin bodies (requires plugins to be installed)") 17 | rootCmd.AddCommand(lintCmd) 18 | } 19 | 20 | var lintCmd = &cobra.Command{ 21 | Use: "lint", 22 | Short: "Evaluate *.fabric files for syntax mistakes", 23 | Long: `Doesn't call plugins, only checks the *.fabric templates for correctness`, 24 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 25 | ctx := cmd.Context() 26 | var diags diagnostics.Diag 27 | eng := engine.New( 28 | engine.WithLogger(slog.Default()), 29 | engine.WithTracer(tracer), 30 | engine.WithBuiltIn(builtin.Plugin(version, slog.Default(), tracer)), 31 | ) 32 | defer func() { 33 | err = exitCommand(eng, cmd, diags) 34 | }() 35 | diag := eng.ParseDir(ctx, cliArgs.sourceDir) 36 | if diags.Extend(diag) { 37 | return 38 | } 39 | if fullLint { 40 | if diags.Extend(eng.LoadPluginResolver(ctx, false)) { 41 | return 42 | } 43 | if diags.Extend(eng.LoadPluginRunner(ctx)) { 44 | return 45 | } 46 | } 47 | diag = eng.Lint(ctx, fullLint) 48 | diags.Extend(diag) 49 | return 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /cmd/validations.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "slices" 9 | "strings" 10 | 11 | "github.com/blackstork-io/fabric/pkg/utils" 12 | ) 13 | 14 | type logLevels struct { 15 | Names []string 16 | Vals []slog.Level 17 | } 18 | 19 | func (ll *logLevels) Find(name string) (level slog.Level, err error) { 20 | nameKey := strings.ToLower(strings.TrimSpace(rawArgs.logLevel)) 21 | idx := slices.Index(ll.Names, nameKey) 22 | if idx == -1 { 23 | err = fmt.Errorf("unknown log level '%s'", name) 24 | return 25 | } 26 | return ll.Vals[idx], nil 27 | } 28 | 29 | func (ll *logLevels) String() string { 30 | return utils.JoinSurround(", ", "'", ll.Names...) 31 | } 32 | 33 | var validLogLevels = logLevels{ 34 | Names: []string{ 35 | "debug", 36 | "info", 37 | "warn", 38 | "error", 39 | }, 40 | Vals: []slog.Level{ 41 | slog.LevelDebug, 42 | slog.LevelInfo, 43 | slog.LevelWarn, 44 | slog.LevelError, 45 | }, 46 | } 47 | 48 | func validateDir(what, dir string) error { 49 | info, err := os.Stat(dir) 50 | switch { 51 | case err == nil: 52 | case errors.Is(err, os.ErrNotExist): 53 | return fmt.Errorf("failed to open %s: path '%s' doesn't exist", what, dir) 54 | case errors.Is(err, os.ErrPermission): 55 | return fmt.Errorf("failed to open %s: permission to access path '%s' denied", what, dir) 56 | default: 57 | return fmt.Errorf("failed to open %s: path '%s': %w", what, dir, err) 58 | } 59 | if !info.IsDir() { 60 | return fmt.Errorf("failed to open %s: path '%s' is not a directory", what, dir) 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | "slices" 7 | "strings" 8 | ) 9 | 10 | // Overridden by goreleaser. 11 | var ( 12 | version = "" 13 | builtBy = "golang" 14 | ) 15 | 16 | func init() { 17 | if builtBy != "goreleaser" { 18 | version = fmt.Sprintf( 19 | "%s+builtBy.%s", 20 | versionFromBuildInfo(), 21 | builtBy, 22 | ) 23 | } 24 | // Version needs to be set here to the command instead of where rootCmd is defined 25 | // because the version is set after the rootCmd is defined. Else, the version 26 | // will be empty and the command will not show the version. 27 | rootCmd.Version = version 28 | } 29 | 30 | func versionFromBuildInfo() (result string) { 31 | result = "v0.0.0-dev" 32 | info, ok := debug.ReadBuildInfo() 33 | if !ok || info == nil { 34 | return 35 | } 36 | if info.Main.Version != "(devel)" { 37 | result = strings.ToLower(info.Main.Version) 38 | if !strings.HasPrefix(result, "v") { 39 | result = "v" + result 40 | } 41 | return 42 | } 43 | var meta []string 44 | // It's a dev version not built by goreleaser, add extra info 45 | dirtyIdx := slices.IndexFunc(info.Settings, func(s debug.BuildSetting) bool { 46 | return s.Key == "vcs.modified" 47 | }) 48 | if dirtyIdx != -1 && info.Settings[dirtyIdx].Value == "true" { 49 | meta = append(meta, "dirty") 50 | } 51 | 52 | shaIdx := slices.IndexFunc(info.Settings, func(s debug.BuildSetting) bool { 53 | return s.Key == "vcs.revision" 54 | }) 55 | if shaIdx != -1 { 56 | meta = append(meta, "rev", info.Settings[shaIdx].Value) 57 | } 58 | if len(meta) != 0 { 59 | result += "+" + strings.Join(meta, ".") 60 | } 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /codegen/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Formats the code 3 | set -e 4 | cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 5 | source ./setup.sh 6 | cd .. 7 | 8 | gofumpt -w . 9 | gci write --skip-generated -s standard -s default -s "prefix(github.com/blackstork-io/fabric)" . 10 | 11 | -------------------------------------------------------------------------------- /codegen/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Runs codegen and docgen tools 3 | set -e 4 | cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 5 | 6 | ./gen_code.sh 7 | ./gen_docs.sh 8 | -------------------------------------------------------------------------------- /codegen/gen_code.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Runs codegen tools 3 | set -e 4 | cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 5 | source ./setup.sh 6 | cd .. 7 | 8 | buf generate 9 | mockery 10 | go mod tidy -------------------------------------------------------------------------------- /codegen/gen_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Runs codegen tools 3 | set -e 4 | cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 5 | 6 | go run ./tools/docgen --version $(git tag -l --sort=-creatordate | head -n1) --output ./docs/plugins -------------------------------------------------------------------------------- /codegen/lint_protobuf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Runs protobuf linter 3 | set -e 4 | cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 5 | source ./setup.sh 6 | cd .. 7 | 8 | if is_ci; then 9 | buf lint --error-format=github-actions 10 | else 11 | buf lint 12 | fi -------------------------------------------------------------------------------- /codegen/rm_gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Removes all generated files 3 | 4 | cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 5 | 6 | grep -R --with-filename --files-with-matches --no-messages --include "*.go" -E -e '^// Code generated .* DO NOT EDIT.$' . | xargs rm 7 | find ./mocks -type d -empty -exec rmdir {} + 8 | 9 | find ./docs/plugins -mindepth 1 \( -maxdepth 1 -type d -o -not -name "*.md" \) -exec rm -rf {} + 10 | -------------------------------------------------------------------------------- /codegen/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Make codegen tools available 3 | set -e 4 | source "$(dirname "${BASH_SOURCE[0]:-$0}")/utils.sh" 5 | # Codegen tools 6 | install_tool mockery github.com/vektra/mockery/v2 "2.52.2" 7 | install_tool buf github.com/bufbuild/buf/cmd/buf "1.32.2" 8 | # Formatting tools 9 | install_tool gci github.com/daixiang0/gci "0.13.4" 10 | install_tool gofumpt mvdan.cc/gofumpt "0.6.0" 11 | wait 12 | -------------------------------------------------------------------------------- /codegen/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Collection of tools 3 | function is_ci() { 4 | [ -n "$CI" ] && [ -n "$GITHUB_ACTIONS" ] 5 | } 6 | 7 | function install_tool() { 8 | local binary="$1" 9 | local path="$2" 10 | local version="$3" 11 | 12 | if $binary --version 2> /dev/null | grep -q "$version"; then 13 | # binary is already installed and has the correct version 14 | return 15 | fi 16 | if is_ci; then 17 | go install $path@v$version & 18 | return 19 | fi 20 | # avoid installing the binary into global scope 21 | # (perhaps developer has another version of the binary or does not want to install it) 22 | eval "function $binary() { go run \"$path@v$version\" \"\$@\"; }" 23 | } -------------------------------------------------------------------------------- /docs/language/vscode-fabric-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstork-io/fabric/122a20956a140c0413d68c061e56e9825c199082/docs/language/vscode-fabric-screenshot.png -------------------------------------------------------------------------------- /docs/plugins/atlassian/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/atlassian 3 | weight: 20 4 | plugin: 5 | name: blackstork/atlassian 6 | description: "The `atlassian` plugin for Atlassian Cloud." 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/atlassian/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/atlassian" "atlassian" "v0.4.2" >}} 15 | 16 | ## Description 17 | The `atlassian` plugin for Atlassian Cloud. 18 | 19 | ## Installation 20 | 21 | 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: 22 | 23 | ```hcl 24 | fabric { 25 | plugin_versions = { 26 | "blackstork/atlassian" = ">= v0.4.2" 27 | } 28 | } 29 | ``` 30 | 31 | 32 | ## Data sources 33 | 34 | {{< plugin-resources "atlassian" "data-source" >}} 35 | -------------------------------------------------------------------------------- /docs/plugins/builtin/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Built-in 3 | weight: 10 4 | plugin: 5 | name: blackstork/builtin 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/builtin" "builtin" "v0.4.2" >}} 15 | 16 | `fabric` binary includes a set of built-in data sources and content providers, available out-of-the-box. 17 | 18 | 19 | ## Data sources 20 | 21 | {{< plugin-resources "builtin" "data-source" >}} 22 | 23 | ## Content providers 24 | 25 | {{< plugin-resources "builtin" "content-provider" >}} 26 | ## Publishers 27 | 28 | {{< plugin-resources "builtin" "publisher" >}} -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/blockquote.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`blockquote` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Formats text as a block quote" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "blockquote" "content provider" >}} 17 | 18 | ## Description 19 | Formats text as a block quote 20 | 21 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 22 | 23 | 24 | #### Configuration 25 | 26 | The content provider doesn't support any configuration arguments. 27 | 28 | #### Usage 29 | 30 | The content provider supports the following execution arguments: 31 | 32 | ```hcl 33 | content blockquote { 34 | # Required string. 35 | # 36 | # For example: 37 | value = "Text to be formatted as a quote" 38 | } 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`code` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Formats text as code snippet" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "code" "content provider" >}} 17 | 18 | ## Description 19 | Formats text as code snippet 20 | 21 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 22 | 23 | 24 | #### Configuration 25 | 26 | The content provider doesn't support any configuration arguments. 27 | 28 | #### Usage 29 | 30 | The content provider supports the following execution arguments: 31 | 32 | ```hcl 33 | content code { 34 | # Required string. 35 | # 36 | # For example: 37 | value = "Text to be formatted as a code block" 38 | 39 | # Specifiy the language for syntax highlighting 40 | # 41 | # Optional string. 42 | # 43 | # For example: 44 | # language = "python3" 45 | # 46 | # Default value: 47 | language = "" 48 | } 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`frontmatter` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Produces the frontmatter" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "frontmatter" "content provider" >}} 17 | 18 | ## Description 19 | Produces the frontmatter. 20 | 21 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 22 | 23 | 24 | #### Configuration 25 | 26 | The content provider doesn't support any configuration arguments. 27 | 28 | #### Usage 29 | 30 | The content provider supports the following execution arguments: 31 | 32 | ```hcl 33 | content frontmatter { 34 | # Format of the frontmatter. 35 | # 36 | # Optional string. 37 | # Must be one of: "yaml", "toml", "json" 38 | # Default value: 39 | format = "yaml" 40 | 41 | # Arbitrary key-value map to be put in the frontmatter. 42 | # 43 | # Required jq queriable. 44 | # Must be non-empty 45 | # 46 | # For example: 47 | content = { 48 | key = "arbitrary value" 49 | key2 = { 50 | "can be nested" = 42 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/image.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`image` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Returns an image tag" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "image" "content provider" >}} 17 | 18 | ## Description 19 | Returns an image tag 20 | 21 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 22 | 23 | 24 | #### Configuration 25 | 26 | The content provider doesn't support any configuration arguments. 27 | 28 | #### Usage 29 | 30 | The content provider supports the following execution arguments: 31 | 32 | ```hcl 33 | content image { 34 | # Required string. 35 | # Must be non-empty 36 | # 37 | # For example: 38 | src = "https://example.com/img.png" 39 | 40 | # Optional string. 41 | # 42 | # For example: 43 | # alt = "Text description of the image" 44 | # 45 | # Default value: 46 | alt = null 47 | } 48 | ``` 49 | 50 | -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/list.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`list` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Produces a list of items" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "list" "content provider" >}} 17 | 18 | ## Description 19 | Produces a list of items 20 | 21 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 22 | 23 | 24 | #### Configuration 25 | 26 | The content provider doesn't support any configuration arguments. 27 | 28 | #### Usage 29 | 30 | The content provider supports the following execution arguments: 31 | 32 | ```hcl 33 | content list { 34 | # Go template for the item of the list 35 | # 36 | # Optional string. 37 | # 38 | # For example: 39 | # item_template = "[{{.Title}}]({{.URL}})" 40 | # 41 | # Default value: 42 | item_template = "{{.}}" 43 | 44 | # Optional string. 45 | # Must be one of: "unordered", "ordered", "tasklist" 46 | # Default value: 47 | format = "unordered" 48 | 49 | # List of items to render. 50 | # 51 | # Required list of jq queriable. 52 | # Must be non-empty 53 | # 54 | # For example: 55 | items = ["First item", "Second item", "Third item"] 56 | } 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/sleep.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`sleep` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Sleeps for the specified duration. Useful for testing and debugging" 6 | tags: ["debug"] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "sleep" "content provider" >}} 17 | 18 | ## Description 19 | 20 | Sleeps for the specified duration. Useful for testing and debugging. 21 | 22 | 23 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 24 | 25 | 26 | #### Configuration 27 | 28 | The content provider doesn't support any configuration arguments. 29 | 30 | #### Usage 31 | 32 | The content provider supports the following execution arguments: 33 | 34 | ```hcl 35 | content sleep { 36 | # Duration to sleep 37 | # 38 | # Optional string. 39 | # Must be non-empty 40 | # Default value: 41 | duration = "1s" 42 | } 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/text.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`text` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Renders text" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "text" "content provider" >}} 17 | 18 | ## Description 19 | Renders text 20 | 21 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 22 | 23 | 24 | #### Configuration 25 | 26 | The content provider doesn't support any configuration arguments. 27 | 28 | #### Usage 29 | 30 | The content provider supports the following execution arguments: 31 | 32 | ```hcl 33 | content text { 34 | # A string to render. Can use go template syntax. 35 | # 36 | # Required string. 37 | # 38 | # For example: 39 | value = "Hello world!" 40 | } 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /docs/plugins/builtin/content-providers/title.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`title` content provider" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Produces a title" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: content-provider 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "title" "content provider" >}} 17 | 18 | ## Description 19 | 20 | Produces a title. 21 | 22 | The title size after calculations must be in an interval [0; 5] inclusive, where 0 23 | corresponds to the largest size (`

`) and 5 corresponds to (`

`) 24 | 25 | 26 | The content provider is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 27 | 28 | 29 | #### Configuration 30 | 31 | The content provider doesn't support any configuration arguments. 32 | 33 | #### Usage 34 | 35 | The content provider supports the following execution arguments: 36 | 37 | ```hcl 38 | content title { 39 | # Title content 40 | # 41 | # Required string. 42 | # 43 | # For example: 44 | value = "Vulnerability Report" 45 | 46 | # Sets the absolute size of the title. 47 | # If `null` – absoulute title size is determined from the document structure 48 | # 49 | # Optional integer. 50 | # Default value: 51 | absolute_size = null 52 | 53 | # Adjusts the absolute size of the title. 54 | # The value (which may be negative) is added to the `absolute_size` to produce the final title size 55 | # 56 | # Optional integer. 57 | # Default value: 58 | relative_size = 0 59 | } 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /docs/plugins/builtin/data-sources/sleep.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`sleep` data source" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Sleeps for the specified duration. Useful for testing and debugging" 6 | tags: ["debug"] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: data-source 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "sleep" "data source" >}} 17 | 18 | ## Description 19 | 20 | Sleeps for the specified duration. Useful for testing and debugging. 21 | 22 | 23 | The data source is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 24 | 25 | ## Configuration 26 | 27 | The data source doesn't support any configuration arguments. 28 | 29 | ## Usage 30 | 31 | The data source supports the following execution arguments: 32 | 33 | ```hcl 34 | data sleep { 35 | # Duration to sleep 36 | # 37 | # Optional string. 38 | # Must be non-empty 39 | # Default value: 40 | duration = "1s" 41 | } 42 | ``` -------------------------------------------------------------------------------- /docs/plugins/builtin/publishers/hub.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`hub` publisher" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Publish documents to Blackstork Hub." 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: publisher 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "hub" "publisher" >}} 17 | 18 | The publisher is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 19 | 20 | #### Formats 21 | 22 | The publisher supports the following document formats: 23 | 24 | - `unknown` 25 | 26 | To set the output format, specify it inside `publish` block with `format` argument. 27 | 28 | 29 | #### Configuration 30 | 31 | The publisher supports the following configuration arguments: 32 | 33 | ```hcl 34 | config publish hub { 35 | # API url. 36 | # 37 | # Required string. 38 | # Must be non-empty 39 | # 40 | # For example: 41 | api_url = "some string" 42 | 43 | # API url. 44 | # 45 | # Required string. 46 | # Must be non-empty 47 | # 48 | # For example: 49 | api_token = "some string" 50 | } 51 | 52 | ``` 53 | 54 | #### Usage 55 | 56 | The publisher supports the following execution arguments: 57 | 58 | ```hcl 59 | # In addition to the arguments listed, `publish` block accepts `format` argument. 60 | 61 | publish hub { 62 | # Hub Document title override. By default uses title configured in the document. 63 | # 64 | # Optional string. 65 | # Must be non-empty 66 | # Default value: 67 | title = null 68 | } 69 | 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /docs/plugins/builtin/publishers/local_file.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`local_file` publisher" 3 | plugin: 4 | name: blackstork/builtin 5 | description: "Publishes content to local file" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/builtin/" 9 | resource: 10 | type: publisher 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/builtin" "builtin" "v0.4.2" "local_file" "publisher" >}} 17 | 18 | The publisher is built-in, which means it's a part of `fabric` binary. It's available out-of-the-box, no installation required. 19 | 20 | #### Formats 21 | 22 | The publisher supports the following document formats: 23 | 24 | - `md` 25 | - `html` 26 | - `pdf` 27 | 28 | To set the output format, specify it inside `publish` block with `format` argument. 29 | 30 | 31 | #### Configuration 32 | 33 | The publisher doesn't support any configuration arguments. 34 | 35 | #### Usage 36 | 37 | The publisher supports the following execution arguments: 38 | 39 | ```hcl 40 | # In addition to the arguments listed, `publish` block accepts `format` argument. 41 | 42 | publish local_file { 43 | # Path to the file 44 | # 45 | # Required string. 46 | # 47 | # For example: 48 | path = "dist/output.md" 49 | } 50 | 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /docs/plugins/content-providers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Content Providers 3 | description: Explore a wide array of content providers through Fabric plugins. These powerful integrations enable the rendering of document content locally or via external APIs, covering various types including text, tables, graphs, code, and more. Enhance your document generation capabilities with Fabric's versatile content provider plugins. 4 | type: docs 5 | weight: 20 6 | --- 7 | 8 | # Content providers 9 | 10 | Content providers are Fabric content renderers. The providers may generate content locally or use an 11 | external service (like OpenAI API) for content generation. 12 | 13 | ## Available content providers 14 | 15 | {{< plugin-resources "" "content-provider" >}} 16 | -------------------------------------------------------------------------------- /docs/plugins/crowdstrike/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/crowdstrike 3 | weight: 20 4 | plugin: 5 | name: blackstork/crowdstrike 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/crowdstrike/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/crowdstrike" "crowdstrike" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/crowdstrike" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "crowdstrike" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/data-sources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Data Sources 3 | description: Discover a diverse range of data sources within Fabric plugins. These integrations empower you to effortlessly load data from files, external services, APIs, and data storage systems. Simplify your data retrieval process and enhance your document generation workflow with Fabric's versatile data sources. 4 | type: docs 5 | weight: 10 6 | --- 7 | 8 | # Data sources 9 | 10 | Data sources are Fabric integrations responsible for loading data from local and external sources - 11 | files, external services, APIs, or data storage solutions. 12 | 13 | ## Available data sources 14 | 15 | {{< plugin-resources "" "data-source" >}} 16 | -------------------------------------------------------------------------------- /docs/plugins/elastic/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/elastic 3 | weight: 20 4 | plugin: 5 | name: blackstork/elastic 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/elastic/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/elastic" "elastic" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/elastic" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "elastic" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/github/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/github 3 | weight: 20 4 | plugin: 5 | name: blackstork/github 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/github/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/github" "github" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/github" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "github" "data-source" >}} 32 | 33 | ## Publishers 34 | 35 | {{< plugin-resources "github" "publisher" >}} -------------------------------------------------------------------------------- /docs/plugins/graphql/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/graphql 3 | weight: 20 4 | plugin: 5 | name: blackstork/graphql 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/graphql/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/graphql" "graphql" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/graphql" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "graphql" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/graphql/data-sources/graphql.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`graphql` data source" 3 | plugin: 4 | name: blackstork/graphql 5 | description: "" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/graphql/" 9 | resource: 10 | type: data-source 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/graphql" "graphql" "v0.4.2" "graphql" "data source" >}} 17 | 18 | ## Installation 19 | 20 | To use `graphql` data source, you must install the plugin `blackstork/graphql`. 21 | 22 | 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: 23 | 24 | ```hcl 25 | fabric { 26 | plugin_versions = { 27 | "blackstork/graphql" = ">= v0.4.2" 28 | } 29 | } 30 | ``` 31 | 32 | Note the version constraint set for the plugin. 33 | 34 | ## Configuration 35 | 36 | The data source supports the following configuration arguments: 37 | 38 | ```hcl 39 | config data graphql { 40 | # Required string. 41 | # 42 | # For example: 43 | url = "some string" 44 | 45 | # Optional string. 46 | # Default value: 47 | auth_token = null 48 | } 49 | ``` 50 | 51 | ## Usage 52 | 53 | The data source supports the following execution arguments: 54 | 55 | ```hcl 56 | data graphql { 57 | # Required string. 58 | # 59 | # For example: 60 | query = "some string" 61 | } 62 | ``` -------------------------------------------------------------------------------- /docs/plugins/hackerone/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/hackerone 3 | weight: 20 4 | plugin: 5 | name: blackstork/hackerone 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/hackerone/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/hackerone" "hackerone" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/hackerone" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "hackerone" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/iris/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/iris 3 | weight: 20 4 | plugin: 5 | name: blackstork/iris 6 | description: "The `iris` plugin for Iris Incident Response platform." 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/iris/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/iris" "iris" "v0.4.2" >}} 15 | 16 | ## Description 17 | The `iris` plugin for Iris Incident Response platform. 18 | 19 | ## Installation 20 | 21 | 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: 22 | 23 | ```hcl 24 | fabric { 25 | plugin_versions = { 26 | "blackstork/iris" = ">= v0.4.2" 27 | } 28 | } 29 | ``` 30 | 31 | 32 | ## Data sources 33 | 34 | {{< plugin-resources "iris" "data-source" >}} 35 | -------------------------------------------------------------------------------- /docs/plugins/microsoft/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/microsoft 3 | weight: 20 4 | plugin: 5 | name: blackstork/microsoft 6 | description: "Plugin for Microsoft services." 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/microsoft/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/microsoft" "microsoft" "v0.4.2" >}} 15 | 16 | ## Description 17 | Plugin for Microsoft services. 18 | 19 | ## Installation 20 | 21 | 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: 22 | 23 | ```hcl 24 | fabric { 25 | plugin_versions = { 26 | "blackstork/microsoft" = ">= v0.4.2" 27 | } 28 | } 29 | ``` 30 | 31 | 32 | ## Data sources 33 | 34 | {{< plugin-resources "microsoft" "data-source" >}} 35 | 36 | ## Content providers 37 | 38 | {{< plugin-resources "microsoft" "content-provider" >}} -------------------------------------------------------------------------------- /docs/plugins/misp/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/misp 3 | weight: 20 4 | plugin: 5 | name: blackstork/misp 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/misp/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/misp" "misp" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/misp" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "misp" "data-source" >}} 32 | 33 | ## Publishers 34 | 35 | {{< plugin-resources "misp" "publisher" >}} -------------------------------------------------------------------------------- /docs/plugins/nist_nvd/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/nist_nvd 3 | weight: 20 4 | plugin: 5 | name: blackstork/nist_nvd 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/nistnvd/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/nist_nvd" "nist_nvd" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/nist_nvd" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "nist_nvd" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/openai/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/openai 3 | weight: 20 4 | plugin: 5 | name: blackstork/openai 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/openai/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/openai" "openai" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/openai" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | 30 | ## Content providers 31 | 32 | {{< plugin-resources "openai" "content-provider" >}} -------------------------------------------------------------------------------- /docs/plugins/opencti/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/opencti 3 | weight: 20 4 | plugin: 5 | name: blackstork/opencti 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/opencti/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/opencti" "opencti" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/opencti" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "opencti" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/opencti/data-sources/opencti.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`opencti` data source" 3 | plugin: 4 | name: blackstork/opencti 5 | description: "" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/opencti/" 9 | resource: 10 | type: data-source 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/opencti" "opencti" "v0.4.2" "opencti" "data source" >}} 17 | 18 | ## Installation 19 | 20 | To use `opencti` data source, you must install the plugin `blackstork/opencti`. 21 | 22 | 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: 23 | 24 | ```hcl 25 | fabric { 26 | plugin_versions = { 27 | "blackstork/opencti" = ">= v0.4.2" 28 | } 29 | } 30 | ``` 31 | 32 | Note the version constraint set for the plugin. 33 | 34 | ## Configuration 35 | 36 | The data source supports the following configuration arguments: 37 | 38 | ```hcl 39 | config data opencti { 40 | # Required string. 41 | # 42 | # For example: 43 | graphql_url = "some string" 44 | 45 | # Optional string. 46 | # Default value: 47 | auth_token = null 48 | } 49 | ``` 50 | 51 | ## Usage 52 | 53 | The data source supports the following execution arguments: 54 | 55 | ```hcl 56 | data opencti { 57 | # Required string. 58 | # 59 | # For example: 60 | graphql_query = "some string" 61 | } 62 | ``` -------------------------------------------------------------------------------- /docs/plugins/postgresql/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/postgresql 3 | weight: 20 4 | plugin: 5 | name: blackstork/postgresql 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/postgresql/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/postgresql" "postgresql" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/postgresql" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "postgresql" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/postgresql/data-sources/postgresql.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`postgresql` data source" 3 | plugin: 4 | name: blackstork/postgresql 5 | description: "" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/postgresql/" 9 | resource: 10 | type: data-source 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/postgresql" "postgresql" "v0.4.2" "postgresql" "data source" >}} 17 | 18 | ## Installation 19 | 20 | To use `postgresql` data source, you must install the plugin `blackstork/postgresql`. 21 | 22 | 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: 23 | 24 | ```hcl 25 | fabric { 26 | plugin_versions = { 27 | "blackstork/postgresql" = ">= v0.4.2" 28 | } 29 | } 30 | ``` 31 | 32 | Note the version constraint set for the plugin. 33 | 34 | ## Configuration 35 | 36 | The data source supports the following configuration arguments: 37 | 38 | ```hcl 39 | config data postgresql { 40 | # Required string. 41 | # Must be non-empty 42 | # 43 | # For example: 44 | database_url = "some string" 45 | } 46 | ``` 47 | 48 | ## Usage 49 | 50 | The data source supports the following execution arguments: 51 | 52 | ```hcl 53 | data postgresql { 54 | # Required string. 55 | # Must be non-empty 56 | # 57 | # For example: 58 | sql_query = "some string" 59 | 60 | # Optional list of any single type. 61 | # Default value: 62 | sql_args = null 63 | } 64 | ``` -------------------------------------------------------------------------------- /docs/plugins/publishers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Publishers 3 | description: A wide range of publishers served by Fabric plugins allow users to publish the documents for storage or dissemination to local and external destinations. 4 | type: docs 5 | weight: 30 6 | --- 7 | 8 | # Publishers 9 | 10 | Publishers are Fabric outgoing integrations, responsible for delivering produced documents to local 11 | and external destinations, for storage or dissemination. 12 | 13 | ## Available publishers 14 | 15 | {{< plugin-resources "" "publisher" >}} 16 | -------------------------------------------------------------------------------- /docs/plugins/snyk/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/snyk 3 | weight: 20 4 | plugin: 5 | name: blackstork/snyk 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/snyk/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/snyk" "snyk" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/snyk" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "snyk" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/splunk/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/splunk 3 | weight: 20 4 | plugin: 5 | name: blackstork/splunk 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/splunk/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/splunk" "splunk" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/splunk" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "splunk" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/sqlite/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/sqlite 3 | weight: 20 4 | plugin: 5 | name: blackstork/sqlite 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/sqlite/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/sqlite" "sqlite" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/sqlite" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "sqlite" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/sqlite/data-sources/sqlite.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`sqlite` data source" 3 | plugin: 4 | name: blackstork/sqlite 5 | description: "" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/sqlite/" 9 | resource: 10 | type: data-source 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/sqlite" "sqlite" "v0.4.2" "sqlite" "data source" >}} 17 | 18 | ## Installation 19 | 20 | To use `sqlite` data source, you must install the plugin `blackstork/sqlite`. 21 | 22 | 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: 23 | 24 | ```hcl 25 | fabric { 26 | plugin_versions = { 27 | "blackstork/sqlite" = ">= v0.4.2" 28 | } 29 | } 30 | ``` 31 | 32 | Note the version constraint set for the plugin. 33 | 34 | ## Configuration 35 | 36 | The data source supports the following configuration arguments: 37 | 38 | ```hcl 39 | config data sqlite { 40 | # Required string. 41 | # Must be non-empty 42 | # 43 | # For example: 44 | database_uri = "some string" 45 | } 46 | ``` 47 | 48 | ## Usage 49 | 50 | The data source supports the following execution arguments: 51 | 52 | ```hcl 53 | data sqlite { 54 | # SQL query to execute 55 | # 56 | # Required string. 57 | # 58 | # For example: 59 | sql_query = "some string" 60 | 61 | # A tuple (or list) of strings, numbers, or booleans to be used as arguments in the SQL query 62 | # 63 | # Optional any type. 64 | # 65 | # For example: 66 | # sql_args = ["example argument", 2, false] 67 | # 68 | # Default value: 69 | sql_args = null 70 | } 71 | ``` -------------------------------------------------------------------------------- /docs/plugins/stixview/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/stixview 3 | weight: 20 4 | plugin: 5 | name: blackstork/stixview 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/stixview/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/stixview" "stixview" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/stixview" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | 30 | ## Content providers 31 | 32 | {{< plugin-resources "stixview" "content-provider" >}} -------------------------------------------------------------------------------- /docs/plugins/terraform/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/terraform 3 | weight: 20 4 | plugin: 5 | name: blackstork/terraform 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/terraform/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/terraform" "terraform" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/terraform" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "terraform" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/terraform/data-sources/terraform_state_local.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`terraform_state_local` data source" 3 | plugin: 4 | name: blackstork/terraform 5 | description: "" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/terraform/" 9 | resource: 10 | type: data-source 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/terraform" "terraform" "v0.4.2" "terraform_state_local" "data source" >}} 17 | 18 | ## Installation 19 | 20 | To use `terraform_state_local` data source, you must install the plugin `blackstork/terraform`. 21 | 22 | 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: 23 | 24 | ```hcl 25 | fabric { 26 | plugin_versions = { 27 | "blackstork/terraform" = ">= v0.4.2" 28 | } 29 | } 30 | ``` 31 | 32 | Note the version constraint set for the plugin. 33 | 34 | ## Configuration 35 | 36 | The data source doesn't support any configuration arguments. 37 | 38 | ## Usage 39 | 40 | The data source supports the following execution arguments: 41 | 42 | ```hcl 43 | data terraform_state_local { 44 | # Required string. 45 | # 46 | # For example: 47 | path = "some string" 48 | } 49 | ``` -------------------------------------------------------------------------------- /docs/plugins/virustotal/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blackstork/virustotal 3 | weight: 20 4 | plugin: 5 | name: blackstork/virustotal 6 | description: "" 7 | tags: [] 8 | version: "v0.4.2" 9 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/virustotal/" 10 | type: docs 11 | hideInMenu: true 12 | --- 13 | 14 | {{< plugin-header "blackstork/virustotal" "virustotal" "v0.4.2" >}} 15 | 16 | ## Installation 17 | 18 | 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: 19 | 20 | ```hcl 21 | fabric { 22 | plugin_versions = { 23 | "blackstork/virustotal" = ">= v0.4.2" 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ## Data sources 30 | 31 | {{< plugin-resources "virustotal" "data-source" >}} 32 | -------------------------------------------------------------------------------- /docs/plugins/virustotal/data-sources/virustotal_api_usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "`virustotal_api_usage` data source" 3 | plugin: 4 | name: blackstork/virustotal 5 | description: "" 6 | tags: [] 7 | version: "v0.4.2" 8 | source_github: "https://github.com/blackstork-io/fabric/tree/main/internal/virustotal/" 9 | resource: 10 | type: data-source 11 | type: docs 12 | --- 13 | 14 | {{< breadcrumbs 2 >}} 15 | 16 | {{< plugin-resource-header "blackstork/virustotal" "virustotal" "v0.4.2" "virustotal_api_usage" "data source" >}} 17 | 18 | ## Installation 19 | 20 | To use `virustotal_api_usage` data source, you must install the plugin `blackstork/virustotal`. 21 | 22 | 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: 23 | 24 | ```hcl 25 | fabric { 26 | plugin_versions = { 27 | "blackstork/virustotal" = ">= v0.4.2" 28 | } 29 | } 30 | ``` 31 | 32 | Note the version constraint set for the plugin. 33 | 34 | ## Configuration 35 | 36 | The data source supports the following configuration arguments: 37 | 38 | ```hcl 39 | config data virustotal_api_usage { 40 | # Required string. 41 | # Must be non-empty 42 | # 43 | # For example: 44 | api_key = "some string" 45 | } 46 | ``` 47 | 48 | ## Usage 49 | 50 | The data source supports the following execution arguments: 51 | 52 | ```hcl 53 | data virustotal_api_usage { 54 | # Optional string. 55 | # Default value: 56 | user_id = null 57 | 58 | # Optional string. 59 | # Default value: 60 | group_id = null 61 | 62 | # Optional string. 63 | # Default value: 64 | start_date = null 65 | 66 | # Optional string. 67 | # Default value: 68 | end_date = null 69 | } 70 | ``` -------------------------------------------------------------------------------- /engine/testdata/a.json: -------------------------------------------------------------------------------- 1 | { 2 | "property_for": "a.json" 3 | } -------------------------------------------------------------------------------- /eval/plugin_action.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/parser/definitions" 5 | "github.com/blackstork-io/fabric/plugin/dataspec" 6 | ) 7 | 8 | type PluginAction struct { 9 | Source *definitions.Plugin 10 | PluginName string 11 | BlockName string 12 | Meta *definitions.MetaBlock 13 | Config *dataspec.Block 14 | Args *dataspec.Block 15 | IsIncluded *dataspec.Attr 16 | } 17 | -------------------------------------------------------------------------------- /eval/plugins.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/plugin" 5 | ) 6 | 7 | type DataSources interface { 8 | DataSource(name string) (*plugin.DataSource, bool) 9 | } 10 | 11 | type ContentProviders interface { 12 | ContentProvider(name string) (*plugin.ContentProvider, bool) 13 | } 14 | 15 | type Publishers interface { 16 | Publisher(name string) (*plugin.Publisher, bool) 17 | } 18 | 19 | type Plugins interface { 20 | DataSources 21 | ContentProviders 22 | Publishers 23 | } 24 | -------------------------------------------------------------------------------- /examples/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugin Examples 2 | 3 | ## Directories 4 | 5 | * [/examples/plugins/basic](./basic) - Example on how to make a simple `fabric` plugin using Go. -------------------------------------------------------------------------------- /examples/plugins/basic/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/examples/plugins/basic" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | func main() { 9 | // Serve the plugin using the pluginapiv1 10 | pluginapiv1.Serve( 11 | // Pass the plugin schema to the Serve function 12 | basic.Plugin("0.0.1"), 13 | ) 14 | // Thats it! The plugin is now ready to be used 15 | // Build it and put it in the plugin directory to test it out 16 | // Binary should be placed like this in the plugin directory 17 | // 18 | // /plugins 19 | // /example 20 | // basic@0.0.1 21 | } 22 | -------------------------------------------------------------------------------- /examples/plugins/basic/content_greeting.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | 9 | "github.com/blackstork-io/fabric/pkg/diagnostics" 10 | "github.com/blackstork-io/fabric/plugin" 11 | "github.com/blackstork-io/fabric/plugin/dataspec" 12 | "github.com/blackstork-io/fabric/plugin/dataspec/constraint" 13 | ) 14 | 15 | // makeGreetingContentProvider creates a new content provider that prints out a greeting message 16 | func makeGreetingContentProvider() *plugin.ContentProvider { 17 | return &plugin.ContentProvider{ 18 | // Config is optional, in this case we don't need it 19 | // We only define the schema for the arguments 20 | Args: &dataspec.RootSpec{ 21 | Attrs: []*dataspec.AttrSpec{{ 22 | Name: "name", 23 | Constraints: constraint.RequiredMeaningful, 24 | Doc: `Name of the user`, 25 | ExampleVal: cty.StringVal("John"), 26 | Type: cty.String, 27 | }}, 28 | }, 29 | // Optional: We can also define the schema for the config 30 | ContentFunc: renderGreetingMessage, 31 | } 32 | } 33 | 34 | func renderGreetingMessage(ctx context.Context, params *plugin.ProvideContentParams) (*plugin.ContentResult, diagnostics.Diag) { 35 | // We specified that the "name" attribute is RequiredMeaningful, so we can safely assume 36 | // that it exists, non-null and non-empty, with whitespace trimmed 37 | name := params.Args.GetAttrVal("name").AsString() 38 | return &plugin.ContentResult{ 39 | Content: plugin.NewElementFromMarkdown(fmt.Sprintf("Hello, %s!", name)), 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /examples/plugins/basic/plugin.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import "github.com/blackstork-io/fabric/plugin" 4 | 5 | // Plugin returns the schema and version for the plugin 6 | // This is the entry point for the plugin 7 | func Plugin(version string) *plugin.Schema { 8 | return &plugin.Schema{ 9 | // Name is the name of the plugin 10 | Name: "example/basic", 11 | // Version is the version of the plugin 12 | // It must be a valid semver 13 | Version: version, 14 | // DataSources is a map of data sources that the plugin provides 15 | // The key is the name of the data source and the value is the data source schema 16 | DataSources: plugin.DataSources{ 17 | "basic_random_numbers": makeRandomNumbersDataSource(), 18 | }, 19 | // ContentProviders is a map of content providers that the plugin provides 20 | // The key is the name of the content provider and the value is the content provider schema 21 | ContentProviders: plugin.ContentProviders{ 22 | "basic_greeting": makeGreetingContentProvider(), 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/templates/async/example.fabric: -------------------------------------------------------------------------------- 1 | document "example" { 2 | title = "Document title" 3 | 4 | data sleep data_a { 5 | duration = "2s" 6 | } 7 | data sleep data_b { 8 | duration = "1s" 9 | } 10 | 11 | data sleep data_c { 12 | duration = "1s" 13 | } 14 | 15 | data sleep data_d { 16 | duration = "3s" 17 | } 18 | 19 | content title { 20 | value = "A" 21 | } 22 | 23 | content sleep content_a { 24 | duration = "2s" 25 | } 26 | 27 | content title { 28 | value = "B" 29 | } 30 | 31 | content sleep content_b { 32 | duration = "1.5s" 33 | } 34 | 35 | content title { 36 | value = "C" 37 | } 38 | 39 | content sleep content_c { 40 | duration = "3.5s" 41 | depends_on = [ 42 | "content.sleep.content_a", 43 | "content.sleep.content_b", 44 | ] 45 | } 46 | 47 | content title { 48 | value = "D" 49 | } 50 | 51 | content sleep content_d { 52 | duration = "3s" 53 | } 54 | 55 | section { 56 | title = "Section 1" 57 | 58 | content sleep content_e { 59 | depends_on = ["content.sleep.content_c"] 60 | duration = "0.5s" 61 | } 62 | 63 | section { 64 | title = "Section 1.1" 65 | 66 | content sleep content_f { 67 | duration = "1s" 68 | } 69 | 70 | content sleep content_g { 71 | duration = "1s" 72 | } 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /examples/templates/atlassian/example.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/atlassian" = ">= 0.5 < 1.0 || 0.5.0-rev0" 4 | } 5 | } 6 | 7 | config data jira_issues { 8 | domain = env.JIRA_DOMAIN 9 | account_email = env.JIRA_ACCOUNT_EMAIL 10 | api_token = env.JIRA_API_TOKEN 11 | } 12 | 13 | document "example" { 14 | title = "Using atlassian plugin" 15 | data jira_issues "my_issues" { 16 | expand = "names" 17 | fields = ["*all"] 18 | jql = "project = TEST" 19 | size = 5 20 | } 21 | content title { 22 | value = "My Jira Issues" 23 | } 24 | content list { 25 | item_template = "{{.key}}: {{.fields.summary}}" 26 | items = query_jq(".data.jira_issues.my_issues") 27 | } 28 | } -------------------------------------------------------------------------------- /examples/templates/azureopenai/example.fabric: -------------------------------------------------------------------------------- 1 | 2 | fabric { 3 | plugin_versions = { 4 | "blackstork/microsoft" = ">= 0.4 < 1.0 || 0.4.0-rev0" 5 | } 6 | } 7 | 8 | document "example" { 9 | meta { 10 | name = "example_document" 11 | } 12 | 13 | title = "Document title" 14 | 15 | section { 16 | title = "Section 2" 17 | 18 | section { 19 | title = "Subsection 2" 20 | 21 | content text { 22 | value = "Text value 4" 23 | } 24 | } 25 | } 26 | 27 | content azure_openai_text { 28 | config { 29 | api_key = env.AZURE_OPENAI_KEY 30 | resource_endpoint = env.AZURE_OPENAI_ENDPOINT 31 | deployment_name = env.AZURE_OPENAI_DEPLOYMENT 32 | api_version = "2024-02-01" 33 | } 34 | prompt = "How are you today?" 35 | max_tokens = 10 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /examples/templates/basic_hello/hello.fabric: -------------------------------------------------------------------------------- 1 | document "hello" { 2 | title = "Welcome" 3 | content text { 4 | value = "Hello from fabric" 5 | } 6 | } -------------------------------------------------------------------------------- /examples/templates/crowdstrike/data_falcon_cspm_ioms.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/crowdstrike" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | document "cspm_ioms" { 8 | meta { 9 | name = "example_document" 10 | } 11 | 12 | data falcon_cspm_ioms "cspm" { 13 | config { 14 | client_id = "" 15 | client_secret = "" 16 | client_cloud = "eu-1" 17 | } 18 | limit = 100 19 | } 20 | 21 | title = "List of CSPM IOMS" 22 | 23 | content table { 24 | rows = query_jq(".data.falcon_cspm_ioms.cspm") 25 | columns = [ 26 | { 27 | "header" = "Account Id" 28 | "value" = "{{.row.value.account_id}}" 29 | }, 30 | { 31 | "header" = "Cloud Provider" 32 | "value" = "{{.row.value.cloud_provider}}" 33 | } 34 | ] 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /examples/templates/crowdstrike/data_falcon_detection_details.fabric: -------------------------------------------------------------------------------- 1 | document "detection_details" { 2 | meta { 3 | name = "example_document" 4 | } 5 | 6 | data falcon_detection_details "details" { 7 | config { 8 | client_id = "90f13a00b72b4306906c9580a24ae0d7" 9 | client_secret = "7X1M6lI4PtU9v5ObmJ8HCSB2jnLqzfacpGx30NWD" 10 | client_cloud = "eu-1" 11 | } 12 | limit = 100 13 | } 14 | 15 | title = "List of Detections" 16 | 17 | content table { 18 | rows = query_jq(".data.falcon_detection_details.details") 19 | columns = [ 20 | { 21 | "header" = "CID" 22 | "value" = "{{.row.value.cid}}" 23 | }, 24 | { 25 | "header" = "Status" 26 | "value" = "{{.row.value.status}}" 27 | } 28 | ] 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /examples/templates/crowdstrike/data_falcon_discover_host_details.fabric: -------------------------------------------------------------------------------- 1 | document "falcon_discover_host_details" { 2 | meta { 3 | name = "example_document" 4 | } 5 | 6 | data falcon_discover_host_details "fdhd" { 7 | config { 8 | client_id = "" 9 | client_secret = "" 10 | client_cloud = "eu-1" 11 | } 12 | limit = 100 13 | } 14 | 15 | title = "List of discover host details" 16 | 17 | content table { 18 | rows = query_jq(".data.falcon_discover_host_details.fdhd") 19 | columns = [ 20 | { 21 | "header" = "Cid" 22 | "value" = "{{.row.value.cid}}" 23 | }, 24 | { 25 | "header" = "City" 26 | "value" = "{{.row.value.city}}" 27 | } 28 | ] 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /examples/templates/crowdstrike/data_falcon_intel_indicators.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/crowdstrike" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | document "intel_indicators" { 8 | meta { 9 | name = "example_document" 10 | } 11 | 12 | data falcon_intel_indicators "indicators" { 13 | config { 14 | client_id = "" 15 | client_secret = "" 16 | client_cloud = "eu-1" 17 | } 18 | limit = 100 19 | } 20 | 21 | title = "List of Intel Indicators" 22 | 23 | content table { 24 | rows = query_jq(".data.falcon_intel_indicators.indicators") 25 | columns = [ 26 | { 27 | "header" = "Id" 28 | "value" = "{{.row.value.id}}" 29 | }, 30 | { 31 | "header" = "Indicator" 32 | "value" = "{{.row.value.indicator}}" 33 | } 34 | ] 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /examples/templates/crowdstrike/data_falcon_vulnerabilities.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/crowdstrike" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | document "vulnerabilities" { 8 | meta { 9 | name = "example_document" 10 | } 11 | 12 | data falcon_vulnerabilities "vulnerabilities" { 13 | config { 14 | client_id = "" 15 | client_secret = "" 16 | client_cloud = "eu-1" 17 | } 18 | limit = 100 19 | } 20 | 21 | title = "List of Falcon vulnerabilities" 22 | 23 | content table { 24 | rows = query_jq(".data.falcon_vulnerabilities.vulnerabilities") 25 | columns = [ 26 | { 27 | "header" = "Id" 28 | "value" = "{{.row.value.id}}" 29 | }, 30 | { 31 | "header" = "Status" 32 | "value" = "{{.row.value.status}}" 33 | } 34 | ] 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /examples/templates/frontmatter/example.fabric: -------------------------------------------------------------------------------- 1 | 2 | document "example" { 3 | title = "Using FrontMatter content provider" 4 | 5 | content frontmatter { 6 | format = "yaml" 7 | content = { 8 | some = "value" 9 | other = "value2" 10 | } 11 | } 12 | 13 | content title { 14 | value = "Sub title 1" 15 | } 16 | 17 | content text { 18 | value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor dolore magna." 19 | } 20 | 21 | content title { 22 | value = "Sub title 2" 23 | } 24 | 25 | content text { 26 | value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor dolore magna." 27 | } 28 | 29 | 30 | } -------------------------------------------------------------------------------- /examples/templates/hackerone/example.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/hackerone" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | config data hackerone_reports { 8 | api_username = env.HACKERONE_API_USERNAME 9 | api_token = env.HACKERONE_API_TOKEN 10 | } 11 | 12 | document "example" { 13 | title = "Using hackerone plugin" 14 | data hackerone_reports "my_reports" { 15 | program = [env.HACKERONE_PROGRAM] 16 | } 17 | content title { 18 | value = "My HackerOne Reports" 19 | } 20 | content list { 21 | query = "[.data.hackerone_reports.my_reports[].attributes.title]" 22 | item_template = "{{.}}" 23 | } 24 | } -------------------------------------------------------------------------------- /examples/templates/hub/example.fabric: -------------------------------------------------------------------------------- 1 | document "example" { 2 | 3 | title = "Example Report, {{ now | date \"Jan 02\" }}" 4 | 5 | publish hub { 6 | config { 7 | api_url = env.HUB_API_URL 8 | api_token = env.HUB_API_TOKEN 9 | } 10 | } 11 | 12 | content title { 13 | value = "TOC" 14 | } 15 | 16 | content toc {} 17 | 18 | content title { 19 | value = "Subtitle 0" 20 | } 21 | 22 | section { 23 | title = "Section #1" 24 | 25 | content title { 26 | value = "Section TOC" 27 | } 28 | 29 | content toc { 30 | start_level = 1 31 | end_level = 4 32 | ordered = true 33 | } 34 | 35 | content text { 36 | value = "Text value 1" 37 | } 38 | 39 | content title { 40 | value = "Subtitle 1" 41 | } 42 | 43 | content text { 44 | value = "Text value 2" 45 | } 46 | 47 | section { 48 | title = "Subsection 1" 49 | 50 | content text { 51 | value = "Text value 3" 52 | } 53 | } 54 | 55 | content title { 56 | value = "Subtitle 2" 57 | } 58 | } 59 | 60 | section { 61 | title = "Section 2" 62 | 63 | section { 64 | title = "Subsection 2" 65 | 66 | content text { 67 | value = "Text value 4" 68 | } 69 | } 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /examples/templates/iris/example.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/iris" = ">= 0.5 < 1.0 || 0.5.0-rev0" 4 | } 5 | } 6 | 7 | config data iris_cases { 8 | api_url = env.IRIS_API_URL 9 | api_key = env.IRIS_API_KEY 10 | # insecure = true 11 | } 12 | 13 | document "example" { 14 | title = "Using iris plugin" 15 | data iris_cases "my_cases" { 16 | size = 2 17 | } 18 | content title { 19 | value = "My Iris Cases" 20 | } 21 | content list { 22 | item_template = "{{.name}}" 23 | items = query_jq(".data.iris_cases.my_cases") 24 | } 25 | } -------------------------------------------------------------------------------- /examples/templates/microsoft/graph_data_source.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/microsoft" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | document "example" { 8 | meta { 9 | name = "example_document" 10 | } 11 | 12 | data microsoft_graph "mygraph" { 13 | config { 14 | client_id = "" 15 | client_secret = "" 16 | tenant_id = "" 17 | # private_key_file = "" 18 | } 19 | api_version = "v1.0" 20 | endpoint = "/security/incidents" 21 | query_params = { 22 | "$top" = "10" 23 | } 24 | } 25 | 26 | title = "List of Security Incidents" 27 | 28 | content table { 29 | rows = query_jq(".data.microsoft_graph.mygraph.value") 30 | columns = [ 31 | { 32 | "header" = "Severity" 33 | "value" = "{{.row.value.severity}}" 34 | }, 35 | { 36 | "header" = "Display Name" 37 | "value" = "{{.row.value.displayName}}" 38 | } 39 | ] 40 | } 41 | 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /examples/templates/misp/misp_event_reports.fabric: -------------------------------------------------------------------------------- 1 | document "misp_event_reports" { 2 | meta { 3 | name = "example_document" 4 | } 5 | 6 | title = "Publish" 7 | 8 | publish misp_event_reports "myreport" { 9 | format = "md" 10 | event_id = "1" 11 | name = "doc.md" 12 | distribution = "0" 13 | config { 14 | api_key = "" 15 | base_url = "https://localhost" 16 | skip_ssl = true 17 | } 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /examples/templates/misp/misp_events.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/misp" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | document "misp_events" { 8 | meta { 9 | name = "example_document" 10 | } 11 | 12 | data misp_events "events" { 13 | value = "" 14 | config { 15 | api_key = "" 16 | base_url = "https://localhost" 17 | skip_ssl = true 18 | } 19 | 20 | limit = 100 21 | } 22 | 23 | title = "List of Events" 24 | 25 | content table { 26 | rows = query_jq(".data.misp_events.events.response") 27 | columns = [ 28 | { 29 | "header" = "Id" 30 | "value" = "{{.row.value.Event.id}}" 31 | }, 32 | { 33 | "header" = "Date" 34 | "value" = "{{.row.value.Event.date}}" 35 | } 36 | ] 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /examples/templates/nist_nvd/example.fabric: -------------------------------------------------------------------------------- 1 | # Example of working with NIST NVD CVES 2 | 3 | fabric { 4 | plugin_versions = { 5 | "blackstork/nist_nvd" = ">= 0.4.1 < 1.0 || 0.4.1-rev0" 6 | "blackstork/openai" = ">= 0.4.1 < 1.0 || 0.4.1-rev0" 7 | } 8 | } 9 | 10 | document "example" { 11 | title = "CVE-2024-29018 NIST NVD CVE vulnerability" 12 | 13 | data nist_nvd_cves "cves" { 14 | cve_id = "CVE-2024-29018" 15 | } 16 | 17 | section { 18 | title = "Description" 19 | content openai_text { 20 | config { 21 | api_key = env.OPENAI_API_KEY 22 | } 23 | query = ".data.nist_nvd_cves.cves" 24 | prompt = "Short description of NIST CVE vulnerability." 25 | } 26 | } 27 | section { 28 | title = "What to do?" 29 | content openai_text { 30 | config { 31 | api_key = env.OPENAI_API_KEY 32 | } 33 | query = ".data.nist_nvd_cves.cves" 34 | prompt = "Step by step guide how to mitigate NIST CVE vulnerability." 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /examples/templates/openai/data.csv: -------------------------------------------------------------------------------- 1 | id,active,name,age,height 2 | b8fa4bb0-6dd4-45ba-96e0-9a182b2b932e,true,Stacey,26,1.98 3 | b0086c49-bcd8-4aae-9f88-4f46b128e709,false,Myriam,33,1.81 4 | a12d2a8c-eebc-42b3-be52-1ab0a2969a81,true,Oralee,31,2.23 -------------------------------------------------------------------------------- /examples/templates/openai/example.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/openai" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | document "example" { 8 | title = "Testing plugins" 9 | 10 | data csv "csv_file" { 11 | path = "./data.csv" 12 | } 13 | content title { 14 | value = "Values from the CSV file" 15 | } 16 | content table { 17 | query = ".data.csv.csv_file" 18 | columns = [ 19 | { 20 | "header" = "ID" 21 | "value" = "ID:{{.id}}" 22 | }, 23 | { 24 | "header" = "Active" 25 | "value" = "{{.active}}" 26 | }, 27 | { 28 | "header" = "Name" 29 | "value" = "{{.name}}" 30 | }, 31 | { 32 | "header" = "Age" 33 | "value" = "{{.age}}" 34 | }, 35 | { 36 | "header" = "Height" 37 | "value" = "{{.height}}" 38 | } 39 | ] 40 | } 41 | content openai_text { 42 | config { 43 | api_key = env.OPENAI_API_KEY 44 | } 45 | query = ".data.csv.csv_file" 46 | model = "gpt-3.5-turbo" 47 | prompt = "Decribe each user in a sentence" 48 | } 49 | } -------------------------------------------------------------------------------- /examples/templates/sections/example.fabric: -------------------------------------------------------------------------------- 1 | document "example" { 2 | title = "Document title" 3 | 4 | content title { 5 | value = "TOC" 6 | } 7 | 8 | content toc {} 9 | 10 | content title { 11 | value = "Subtitle 0" 12 | } 13 | 14 | section { 15 | title = "Section #1" 16 | 17 | content title { 18 | value = "Section TOC" 19 | } 20 | 21 | content toc { 22 | start_level = 1 23 | end_level = 4 24 | ordered = true 25 | } 26 | 27 | content text { 28 | value = "Text value 1" 29 | } 30 | 31 | content title { 32 | value = "Subtitle 1" 33 | } 34 | 35 | content text { 36 | value = "Text value 2" 37 | } 38 | 39 | section { 40 | title = "Subsection 1" 41 | 42 | content text { 43 | value = "Text value 3" 44 | } 45 | } 46 | 47 | content title { 48 | value = "Subtitle 2" 49 | } 50 | } 51 | 52 | section { 53 | title = "Section 2" 54 | 55 | section { 56 | title = "Subsection 2" 57 | 58 | content text { 59 | value = "Text value 4" 60 | } 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /examples/templates/stixview/example.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/stixview" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | document "example" { 8 | title = "Using stixview plugin" 9 | content stixview { 10 | gist_id = "6a0fbb0f6e7faf063c748b23f9c7dc62" 11 | height = 500 12 | width = 500 13 | } 14 | } -------------------------------------------------------------------------------- /examples/templates/virustotal/example.fabric: -------------------------------------------------------------------------------- 1 | fabric { 2 | plugin_versions = { 3 | "blackstork/virustotal" = ">= 0.4 < 1.0 || 0.4.0-rev0" 4 | } 5 | } 6 | 7 | config data virustotal_api_usage { 8 | api_key = env.VIRUSTOTAL_API_KEY 9 | } 10 | 11 | document "example" { 12 | title = "Using virustotal plugin" 13 | data virustotal_api_usage "my_usage" { 14 | user_id = env.VIRUSTOTAL_USER_ID 15 | start_date = "20240201" 16 | end_date = "20240203" 17 | } 18 | content text { 19 | value = "{{.data.virustotal_api_usage.my_usage.daily}}" 20 | } 21 | } -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run github.com/vektra/mockery/v2@v2.52.2 4 | //go:generate go run github.com/bufbuild/buf/cmd/buf@v1.32.2 generate 5 | -------------------------------------------------------------------------------- /internal/atlassian/client/dto.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func String(s string) *string { 8 | return &s 9 | } 10 | 11 | func Int(i int) *int { 12 | return &i 13 | } 14 | 15 | type Error struct { 16 | ErrorMessages []string `json:"errorMessages"` 17 | } 18 | 19 | func (err *Error) Error() string { 20 | return strings.Join(err.ErrorMessages, " ") 21 | } 22 | 23 | type SearchIssuesReq struct { 24 | Expand *string `json:"expand,omitempty"` 25 | Fields []string `json:"fields,omitempty"` 26 | JQL *string `json:"jql,omitempty"` 27 | Properties []string `json:"properties,omitempty"` 28 | NextPageToken *string `json:"nextPageToken,omitempty"` 29 | MaxResults *int `json:"maxResults,omitempty"` 30 | } 31 | 32 | type SearchIssuesRes struct { 33 | NextPageToken *string `json:"nextPageToken,omitempty"` 34 | Issues []any `json:"issues"` 35 | } 36 | -------------------------------------------------------------------------------- /internal/atlassian/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/atlassian" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | atlassian.Plugin(version, atlassian.DefaultClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/atlassian/plugin_test.go: -------------------------------------------------------------------------------- 1 | package atlassian 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/atlassian", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["jira_issues"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/builtin/content_blockquote.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/zclconf/go-cty/cty" 9 | 10 | "github.com/blackstork-io/fabric/pkg/diagnostics" 11 | "github.com/blackstork-io/fabric/plugin" 12 | "github.com/blackstork-io/fabric/plugin/dataspec" 13 | "github.com/blackstork-io/fabric/plugin/dataspec/constraint" 14 | ) 15 | 16 | func makeBlockQuoteContentProvider() *plugin.ContentProvider { 17 | return &plugin.ContentProvider{ 18 | ContentFunc: genBlockQuoteContent, 19 | Args: &dataspec.RootSpec{ 20 | Attrs: []*dataspec.AttrSpec{{ 21 | Name: "value", 22 | Type: cty.String, 23 | ExampleVal: cty.StringVal("Text to be formatted as a quote"), 24 | Constraints: constraint.RequiredNonNull, 25 | }}, 26 | }, 27 | Doc: "Formats text as a block quote", 28 | } 29 | } 30 | 31 | func genBlockQuoteContent(ctx context.Context, params *plugin.ProvideContentParams) (*plugin.ContentResult, diagnostics.Diag) { 32 | value := params.Args.GetAttrVal("value") 33 | text, err := genTextContentText(value.AsString(), params.DataContext) 34 | if err != nil { 35 | return nil, diagnostics.Diag{{ 36 | Severity: hcl.DiagError, 37 | Summary: "Failed to render blockquote", 38 | Detail: err.Error(), 39 | }} 40 | } 41 | text = "> " + strings.ReplaceAll(text, "\n", "\n> ") 42 | return &plugin.ContentResult{ 43 | Content: plugin.NewElementFromMarkdown(text), 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/builtin/content_code.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/zclconf/go-cty/cty" 9 | 10 | "github.com/blackstork-io/fabric/pkg/diagnostics" 11 | "github.com/blackstork-io/fabric/plugin" 12 | "github.com/blackstork-io/fabric/plugin/dataspec" 13 | "github.com/blackstork-io/fabric/plugin/dataspec/constraint" 14 | ) 15 | 16 | func makeCodeContentProvider() *plugin.ContentProvider { 17 | return &plugin.ContentProvider{ 18 | ContentFunc: genCodeContent, 19 | Args: &dataspec.RootSpec{ 20 | Attrs: []*dataspec.AttrSpec{ 21 | { 22 | Name: "value", 23 | Type: cty.String, 24 | Constraints: constraint.RequiredNonNull, 25 | ExampleVal: cty.StringVal("Text to be formatted as a code block"), 26 | }, 27 | { 28 | Name: "language", 29 | Type: cty.String, 30 | ExampleVal: cty.StringVal("python3"), 31 | DefaultVal: cty.StringVal(""), 32 | Doc: `Specifiy the language for syntax highlighting`, 33 | }, 34 | }, 35 | }, 36 | Doc: "Formats text as code snippet", 37 | } 38 | } 39 | 40 | func genCodeContent(ctx context.Context, params *plugin.ProvideContentParams) (*plugin.ContentResult, diagnostics.Diag) { 41 | value := params.Args.GetAttrVal("value") 42 | lang := params.Args.GetAttrVal("language") 43 | text, err := genTextContentText(value.AsString(), params.DataContext) 44 | if err != nil { 45 | return nil, diagnostics.Diag{{ 46 | Severity: hcl.DiagError, 47 | Summary: "Failed to render code", 48 | Detail: err.Error(), 49 | }} 50 | } 51 | text = fmt.Sprintf("```%s\n%s\n```", lang.AsString(), text) 52 | return &plugin.ContentResult{ 53 | Content: plugin.NewElementFromMarkdown(text), 54 | }, nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/builtin/content_sleep.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | "github.com/zclconf/go-cty/cty" 11 | 12 | "github.com/blackstork-io/fabric/pkg/diagnostics" 13 | "github.com/blackstork-io/fabric/plugin" 14 | "github.com/blackstork-io/fabric/plugin/dataspec" 15 | "github.com/blackstork-io/fabric/plugin/dataspec/constraint" 16 | ) 17 | 18 | func makeSleepContentProvider(logger *slog.Logger) *plugin.ContentProvider { 19 | logger = logger.With("content_provider", "sleep") 20 | 21 | return &plugin.ContentProvider{ 22 | Doc: ` 23 | Sleeps for the specified duration. Useful for testing and debugging. 24 | `, 25 | Tags: []string{"debug"}, 26 | Args: &dataspec.RootSpec{ 27 | Attrs: []*dataspec.AttrSpec{ 28 | { 29 | Name: "duration", 30 | Type: cty.String, 31 | Doc: "Duration to sleep", 32 | Constraints: constraint.Meaningful, 33 | DefaultVal: cty.StringVal("1s"), 34 | }, 35 | }, 36 | }, 37 | ContentFunc: func(ctx context.Context, params *plugin.ProvideContentParams) (*plugin.ContentResult, diagnostics.Diag) { 38 | duration, err := time.ParseDuration(params.Args.GetAttrVal("duration").AsString()) 39 | if err != nil { 40 | return nil, diagnostics.Diag{ 41 | { 42 | Severity: hcl.DiagError, 43 | Summary: "Invalid duration", 44 | Detail: err.Error(), 45 | }, 46 | } 47 | } 48 | 49 | logger.WarnContext(ctx, "Sleeping", "duration", duration) 50 | time.Sleep(duration) 51 | 52 | return &plugin.ContentResult{ 53 | Content: plugin.NewElementFromMarkdown( 54 | fmt.Sprintf("Slept for %s.", duration), 55 | ), 56 | }, nil 57 | }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/builtin/hubapi/dto.go: -------------------------------------------------------------------------------- 1 | package hubapi 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type request struct { 10 | Params any `json:"params"` 11 | } 12 | 13 | type response struct { 14 | Data json.RawMessage `json:"data,omitempty"` 15 | Error *Error `json:"error,omitempty"` 16 | } 17 | 18 | type Error struct { 19 | Details []*ErrorDetail `json:"details"` 20 | } 21 | 22 | type ErrorDetail struct { 23 | Message string `json:"message"` 24 | } 25 | 26 | func (err *Error) Error() string { 27 | messages := make([]string, len(err.Details)) 28 | for i, detail := range err.Details { 29 | messages[i] = detail.Message 30 | } 31 | return strings.Join(messages, "; ") 32 | } 33 | 34 | type Document struct { 35 | ID string `json:"id"` 36 | Title string `json:"title"` 37 | ContentID *string `json:"content_id"` 38 | CreatedAt time.Time `json:"created_at"` 39 | UpdatedAt time.Time `json:"updated_at"` 40 | } 41 | 42 | type DocumentParams struct { 43 | Title string `json:"title"` 44 | } 45 | 46 | type DocumentContent struct { 47 | ID string `json:"id"` 48 | CreatedAt time.Time `json:"created_at"` 49 | } 50 | -------------------------------------------------------------------------------- /internal/builtin/plugin.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "go.opentelemetry.io/otel/trace" 7 | 8 | "github.com/blackstork-io/fabric/plugin" 9 | ) 10 | 11 | const Name = "blackstork/builtin" 12 | 13 | func Plugin(version string, logger *slog.Logger, tracer trace.Tracer) *plugin.Schema { 14 | return &plugin.Schema{ 15 | Name: Name, 16 | Version: version, 17 | DataSources: plugin.DataSources{ 18 | "csv": makeCSVDataSource(), 19 | "txt": makeTXTDataSource(), 20 | "rss": makeRSSDataSource(), 21 | "json": makeJSONDataSource(), 22 | "yaml": makeYAMLDataSource(), 23 | "http": makeHTTPDataSource(version), 24 | "sleep": makeSleepDataSource(logger), 25 | }, 26 | ContentProviders: plugin.ContentProviders{ 27 | "toc": makeTOCContentProvider(), 28 | "text": makeTextContentProvider(), 29 | "title": makeTitleContentProvider(), 30 | "code": makeCodeContentProvider(), 31 | "blockquote": makeBlockQuoteContentProvider(), 32 | "image": makeImageContentProvider(), 33 | "list": makeListContentProvider(), 34 | "table": makeTableContentProvider(), 35 | "frontmatter": makeFrontMatterContentProvider(), 36 | "sleep": makeSleepContentProvider(logger), 37 | }, 38 | Publishers: plugin.Publishers{ 39 | "local_file": makeLocalFilePublisher(logger, tracer), 40 | "hub": makeHubPublisher(version, defaultHubClientLoader, logger, tracer), 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/builtin/plugin_test.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "io" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestPluginSchema(t *testing.T) { 12 | schema := Plugin("1.2.3", slog.New(slog.NewTextHandler(io.Discard, nil)), nil) 13 | assert.Equal(t, "blackstork/builtin", schema.Name) 14 | assert.Equal(t, "1.2.3", schema.Version) 15 | assert.NotNil(t, schema.DataSources["csv"]) 16 | assert.NotNil(t, schema.DataSources["txt"]) 17 | assert.NotNil(t, schema.DataSources["json"]) 18 | assert.NotNil(t, schema.DataSources["rss"]) 19 | // Content Providers 20 | assert.NotNil(t, schema.ContentProviders["toc"]) 21 | assert.NotNil(t, schema.ContentProviders["text"]) 22 | assert.NotNil(t, schema.ContentProviders["title"]) 23 | assert.NotNil(t, schema.ContentProviders["code"]) 24 | assert.NotNil(t, schema.ContentProviders["blockquote"]) 25 | assert.NotNil(t, schema.ContentProviders["image"]) 26 | assert.NotNil(t, schema.ContentProviders["list"]) 27 | assert.NotNil(t, schema.ContentProviders["table"]) 28 | assert.NotNil(t, schema.ContentProviders["frontmatter"]) 29 | // Publishers 30 | assert.NotNil(t, schema.Publishers["local_file"]) 31 | assert.NotNil(t, schema.Publishers["hub"]) 32 | } 33 | -------------------------------------------------------------------------------- /internal/builtin/testdata/csv/comma.csv: -------------------------------------------------------------------------------- 1 | id,active,name,age,height 2 | b8fa4bb0-6dd4-45ba-96e0-9a182b2b932e,true,Stacey,26,1.98 3 | b0086c49-bcd8-4aae-9f88-4f46b128e709,false,Myriam,33,1.81 4 | a12d2a8c-eebc-42b3-be52-1ab0a2969a81,true,Oralee,31,2.23 -------------------------------------------------------------------------------- /internal/builtin/testdata/csv/empty.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstork-io/fabric/122a20956a140c0413d68c061e56e9825c199082/internal/builtin/testdata/csv/empty.csv -------------------------------------------------------------------------------- /internal/builtin/testdata/csv/invalid.csv: -------------------------------------------------------------------------------- 1 | id,name,age,height 2 | b8fa4bb0-6dd4-45ba-96e0-9a182b2b932e Stacey,26,1.98 3 | b0086c49-bcd8-4aae-9f88-4f46b128e709,Myriam,33,1.81, 4 | a12d2a8c-eebc-42b3-be52-1ab0a2969a81,Oralee,31,2.23 -------------------------------------------------------------------------------- /internal/builtin/testdata/csv/semicolon.csv: -------------------------------------------------------------------------------- 1 | id;active;name;age;height 2 | b8fa4bb0-6dd4-45ba-96e0-9a182b2b932e;true;Stacey;26;1.98 3 | b0086c49-bcd8-4aae-9f88-4f46b128e709;false;Myriam;33;1.81 4 | a12d2a8c-eebc-42b3-be52-1ab0a2969a81;true;Oralee;31;2.23 -------------------------------------------------------------------------------- /internal/builtin/testdata/json/a.json: -------------------------------------------------------------------------------- 1 | { 2 | "property_for": "a.json" 3 | } -------------------------------------------------------------------------------- /internal/builtin/testdata/json/dir/b.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "property_for": "dir/b.json" 5 | }, 6 | { 7 | "id": 2, 8 | "property_for": "dir/b.json" 9 | } 10 | ] -------------------------------------------------------------------------------- /internal/builtin/testdata/json/dir/c.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 3, 4 | "property_for": "dir/c.json" 5 | }, 6 | { 7 | "id": 4, 8 | "property_for": "dir/c.json" 9 | } 10 | ] -------------------------------------------------------------------------------- /internal/builtin/testdata/json/invalid.txt: -------------------------------------------------------------------------------- 1 | {invalid} -------------------------------------------------------------------------------- /internal/builtin/testdata/rss/basic.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example Feed 7 | A subtitle. 8 | 9 | 10 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 11 | 2003-12-13T18:30:02Z 12 | 13 | 14 | 15 | Atom-Powered Robots Run Amok 16 | 17 | 18 | 19 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 20 | 2003-11-09T17:23:02Z 21 | 2003-12-13T18:30:02Z 22 | Some text. 23 | 24 |
25 |

This is the entry content.

26 |
27 |
28 | 29 | John Doe 30 | johndoe@example.com 31 | 32 |
33 | 34 |
35 | -------------------------------------------------------------------------------- /internal/builtin/testdata/rss/basic.rss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RSS Title 5 | This is an example of an RSS feed 6 | http://www.example.com/main.html 7 | 2020 Example.com All rights reserved 8 | Wed, 9 Sep 2010 00:01:00 +0000 9 | Sun, 6 Sep 2009 16:20:12 +0000 10 | 1800 11 | 12 | 13 | Example entry 2 14 | Here is some text containing an interesting description. 15 | {address}/content/2 16 | 4824db5b-6278-48bd-9657-46a66de3dc1a 17 | Tue, 8 Sep 2009 22:00:00 +0000 18 | 19 | 20 | Example entry 1 21 | Here is some text containing an interesting description. 22 | {address}/content/1 23 | 7bd204c6-1655-4c27-aeee-53f933c5395f 24 | Sun, 6 Sep 2009 16:20:23 +0000 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /internal/builtin/testdata/txt/data.txt: -------------------------------------------------------------------------------- 1 | data_content -------------------------------------------------------------------------------- /internal/builtin/testdata/txt/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstork-io/fabric/122a20956a140c0413d68c061e56e9825c199082/internal/builtin/testdata/txt/empty.txt -------------------------------------------------------------------------------- /internal/builtin/testdata/yaml/a.yaml: -------------------------------------------------------------------------------- 1 | property_for: a.yaml 2 | -------------------------------------------------------------------------------- /internal/builtin/testdata/yaml/dir/b.yaml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | property_for: dir/b.yaml 3 | - id: 2 4 | property_for: dir/b.yaml 5 | -------------------------------------------------------------------------------- /internal/builtin/testdata/yaml/dir/c.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: 3 3 | property_for: dir/c.yaml 4 | - id: 4 5 | property_for: dir/c.yaml 6 | -------------------------------------------------------------------------------- /internal/builtin/testdata/yaml/invalid.txt: -------------------------------------------------------------------------------- 1 | foo: 2 | bar 3 | -------------------------------------------------------------------------------- /internal/builtin/utils/parsers.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "encoding/json" 7 | "io" 8 | 9 | "github.com/blackstork-io/fabric/plugin/plugindata" 10 | ) 11 | 12 | func ParseCSVContent(ctx context.Context, reader *csv.Reader) (plugindata.List, error) { 13 | rowMaps := make(plugindata.List, 0) 14 | headers, err := reader.Read() 15 | if err == io.EOF { 16 | return rowMaps, nil 17 | } else if err != nil { 18 | return nil, err 19 | } 20 | 21 | for { 22 | select { 23 | case <-ctx.Done(): // stop reading if the context is canceled 24 | return nil, ctx.Err() 25 | default: 26 | row, err := reader.Read() 27 | if err == io.EOF { 28 | return rowMaps, nil 29 | } else if err != nil { 30 | return nil, err 31 | } 32 | rowMap := make(plugindata.Map, len(headers)) 33 | for j, header := range headers { 34 | if header == "" { 35 | continue 36 | } 37 | if j >= len(row) { 38 | rowMap[header] = nil 39 | continue 40 | } 41 | if row[j] == "true" { 42 | rowMap[header] = plugindata.Bool(true) 43 | } else if row[j] == "false" { 44 | rowMap[header] = plugindata.Bool(false) 45 | } else { 46 | n := json.Number(row[j]) 47 | if f, err := n.Float64(); err == nil { 48 | rowMap[header] = plugindata.Number(f) 49 | } else { 50 | rowMap[header] = plugindata.String(row[j]) 51 | } 52 | } 53 | } 54 | rowMaps = append(rowMaps, rowMap) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/crowdstrike/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/crowdstrike" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | crowdstrike.Plugin(version, nil), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/crowdstrike/plugin_test.go: -------------------------------------------------------------------------------- 1 | package crowdstrike 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/crowdstrike", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | } 14 | -------------------------------------------------------------------------------- /internal/elastic/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/elastic" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | elastic.Plugin(version, elastic.DefaultKibanaClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/elastic/plugin_test.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/elastic", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["elasticsearch"]) 14 | assert.NotNil(t, schema.DataSources["elastic_security_cases"]) 15 | } 16 | -------------------------------------------------------------------------------- /internal/elastic/testdata/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "54f7a815-eac5-4f7c-a339-5fefd0f54967", 4 | "type": "foo", 5 | "active": false, 6 | "age": 39, 7 | "name": "Davidson" 8 | }, 9 | { 10 | "id": "0c68e63d-daaa-4a62-92e6-e855bd144fb6", 11 | "type": "bar", 12 | "active": false, 13 | "age": 20, 14 | "name": "Thompson" 15 | }, 16 | { 17 | "id": "a117a5e6-23d0-4daa-be3c-a70900ca4163", 18 | "type": "foo", 19 | "active": true, 20 | "age": 21, 21 | "name": "Armstrong" 22 | } 23 | ] -------------------------------------------------------------------------------- /internal/github/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/github" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | github.Plugin(version, nil), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/github/plugin.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | gh "github.com/google/go-github/v58/github" 7 | 8 | "github.com/blackstork-io/fabric/plugin" 9 | ) 10 | 11 | var DefaultClientLoader = func(token string) Client { 12 | return &ClientAdapter{gh.NewClient(nil).WithAuthToken(token)} 13 | } 14 | 15 | const ( 16 | minPage = 1 17 | pageSize = 30 18 | ) 19 | 20 | type ClientLoaderFn func(token string) Client 21 | 22 | type Client interface { 23 | Issues() IssuesClient 24 | Gists() GistClient 25 | } 26 | 27 | type ClientAdapter struct { 28 | gh *gh.Client 29 | } 30 | 31 | func (c *ClientAdapter) Issues() IssuesClient { 32 | return c.gh.Issues 33 | } 34 | 35 | func (c *ClientAdapter) Gists() GistClient { 36 | return c.gh.Gists 37 | } 38 | 39 | type IssuesClient interface { 40 | ListByRepo(ctx context.Context, owner, repo string, opts *gh.IssueListByRepoOptions) ([]*gh.Issue, *gh.Response, error) 41 | } 42 | 43 | type GistClient interface { 44 | Create(ctx context.Context, gist *gh.Gist) (*gh.Gist, *gh.Response, error) 45 | Get(ctx context.Context, id string) (*gh.Gist, *gh.Response, error) 46 | Edit(ctx context.Context, id string, gist *gh.Gist) (*gh.Gist, *gh.Response, error) 47 | } 48 | 49 | func Plugin(version string, clientLoader ClientLoaderFn) *plugin.Schema { 50 | if clientLoader == nil { 51 | clientLoader = DefaultClientLoader 52 | } 53 | return &plugin.Schema{ 54 | Name: "blackstork/github", 55 | Version: version, 56 | DataSources: plugin.DataSources{ 57 | "github_issues": makeGithubIssuesDataSchema(clientLoader), 58 | }, 59 | Publishers: plugin.Publishers{ 60 | "github_gist": makeGithubGistPublisher(clientLoader), 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/github/plugin_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/github", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["github_issues"]) 14 | assert.NotNil(t, schema.Publishers["github_gist"]) 15 | } 16 | -------------------------------------------------------------------------------- /internal/graphql/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/graphql" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | graphql.Plugin(version), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/graphql/plugin.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/plugin" 5 | ) 6 | 7 | func Plugin(version string) *plugin.Schema { 8 | return &plugin.Schema{ 9 | Name: "blackstork/graphql", 10 | Version: version, 11 | DataSources: plugin.DataSources{ 12 | "graphql": makeGraphQLDataSource(), 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/graphql/plugin_test.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3") 11 | assert.Equal(t, "blackstork/graphql", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["graphql"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/hackerone/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/hackerone" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | hackerone.Plugin(version, hackerone.DefaultClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/hackerone/plugin.go: -------------------------------------------------------------------------------- 1 | package hackerone 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/blackstork-io/fabric/internal/hackerone/client" 7 | "github.com/blackstork-io/fabric/plugin" 8 | "github.com/blackstork-io/fabric/plugin/dataspec" 9 | ) 10 | 11 | const ( 12 | minPage = 1 13 | pageSize = 25 14 | ) 15 | 16 | type ClientLoadFn func(user, token string) client.Client 17 | 18 | var DefaultClientLoader ClientLoadFn = client.New 19 | 20 | func Plugin(version string, loader ClientLoadFn) *plugin.Schema { 21 | if loader == nil { 22 | loader = DefaultClientLoader 23 | } 24 | return &plugin.Schema{ 25 | Name: "blackstork/hackerone", 26 | Version: version, 27 | DataSources: plugin.DataSources{ 28 | "hackerone_reports": makeHackerOneReportsDataSchema(loader), 29 | }, 30 | } 31 | } 32 | 33 | func makeClient(loader ClientLoadFn, cfg *dataspec.Block) (client.Client, error) { 34 | if cfg == nil { 35 | return nil, fmt.Errorf("configuration is required") 36 | } 37 | user := cfg.GetAttrVal("api_username") 38 | if user.IsNull() || user.AsString() == "" { 39 | return nil, fmt.Errorf("api_username is required in configuration") 40 | } 41 | token := cfg.GetAttrVal("api_token") 42 | if token.IsNull() || token.AsString() == "" { 43 | return nil, fmt.Errorf("api_token is required in configuration") 44 | } 45 | return loader(user.AsString(), token.AsString()), nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/hackerone/plugin_test.go: -------------------------------------------------------------------------------- 1 | package hackerone 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/hackerone", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["hackerone_reports"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/iris/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/iris" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | iris.Plugin(version, iris.DefaultClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/iris/plugin.go: -------------------------------------------------------------------------------- 1 | package iris 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | 9 | "github.com/blackstork-io/fabric/internal/iris/client" 10 | "github.com/blackstork-io/fabric/pkg/diagnostics" 11 | "github.com/blackstork-io/fabric/plugin" 12 | "github.com/blackstork-io/fabric/plugin/dataspec" 13 | ) 14 | 15 | type ClientLoadFn func(url, apiKey string, insecure bool) client.Client 16 | 17 | var DefaultClientLoader ClientLoadFn = client.New 18 | 19 | func Plugin(version string, loader ClientLoadFn) *plugin.Schema { 20 | if loader == nil { 21 | loader = DefaultClientLoader 22 | } 23 | return &plugin.Schema{ 24 | Name: "blackstork/iris", 25 | Doc: "The `iris` plugin for Iris Incident Response platform.", 26 | Version: version, 27 | DataSources: plugin.DataSources{ 28 | "iris_cases": makeIrisCasesDataSource(loader), 29 | "iris_alerts": makeIrisAlertsDataSource(loader), 30 | }, 31 | } 32 | } 33 | 34 | func parseConfig(cfg *dataspec.Block, loader ClientLoadFn) (client.Client, error) { 35 | if cfg == nil { 36 | return nil, fmt.Errorf("configuration is required") 37 | } 38 | apiURL := cfg.GetAttrVal("api_url").AsString() 39 | apiKey := cfg.GetAttrVal("api_key").AsString() 40 | insecure := cfg.GetAttrVal("insecure").True() 41 | return loader(apiURL, apiKey, insecure), nil 42 | } 43 | 44 | func handleClientError(err error) diagnostics.Diag { 45 | var clientErr *client.Error 46 | if errors.As(err, &clientErr) { 47 | return diagnostics.Diag{{ 48 | Severity: hcl.DiagError, 49 | Summary: "Failed to call Iris API", 50 | Detail: clientErr.Message, 51 | }} 52 | } 53 | return diagnostics.Diag{{ 54 | Severity: hcl.DiagError, 55 | Summary: "Unknown error while calling Iris API", 56 | }} 57 | } 58 | -------------------------------------------------------------------------------- /internal/iris/plugin_test.go: -------------------------------------------------------------------------------- 1 | package iris 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/iris", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["iris_cases"]) 14 | assert.NotNil(t, schema.DataSources["iris_alerts"]) 15 | } 16 | -------------------------------------------------------------------------------- /internal/microsoft/client/azure_client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type ClientTestSuite struct { 14 | suite.Suite 15 | ctx context.Context 16 | cancel context.CancelFunc 17 | } 18 | 19 | func (s *ClientTestSuite) SetupTest() { 20 | s.ctx, s.cancel = context.WithCancel(context.Background()) 21 | } 22 | 23 | func (s *ClientTestSuite) TearDownTest() { 24 | s.cancel() 25 | } 26 | 27 | func TestClientTestSuite(t *testing.T) { 28 | suite.Run(t, new(ClientTestSuite)) 29 | } 30 | 31 | func (s *ClientTestSuite) mock(fn http.HandlerFunc, token string) (azureClient, *httptest.Server) { 32 | srv := httptest.NewServer(fn) 33 | cli := azureClient{ 34 | accessToken: token, 35 | baseURL: srv.URL, 36 | client: &http.Client{}, 37 | } 38 | return cli, srv 39 | } 40 | 41 | func (s *ClientTestSuite) TestPrepare() { 42 | client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { 43 | s.Equal("Bearer test_token", r.Header.Get("Authorization")) 44 | s.Equal("2023-11-01", r.URL.Query().Get("api-version")) 45 | }, "test_token") 46 | defer srv.Close() 47 | client.QueryObjects(s.ctx, "/tmp", url.Values{}, 1) 48 | } 49 | 50 | func (s *ClientTestSuite) TestGetAllReportsError() { 51 | client, srv := s.mock(func(w http.ResponseWriter, r *http.Request) { 52 | w.WriteHeader(http.StatusUnauthorized) 53 | }, "test_token") 54 | defer srv.Close() 55 | _, err := client.QueryObjects(s.ctx, "/tmp", url.Values{}, 1) 56 | s.Error(err) 57 | } 58 | 59 | func (s *ClientTestSuite) TestBaseURL() { 60 | cli := NewAzureClient("dummy-access-token") 61 | s.Equal(baseURLAzure, cli.baseURL) 62 | } 63 | -------------------------------------------------------------------------------- /internal/microsoft/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/microsoft" 5 | "github.com/blackstork-io/fabric/internal/microsoft/client" 6 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 7 | ) 8 | 9 | var version string 10 | 11 | func main() { 12 | pluginapiv1.Serve( 13 | microsoft.Plugin( 14 | version, 15 | microsoft.MakeDefaultAzureClientLoader(client.AcquireAzureToken), 16 | microsoft.MakeAzureOpenAIClientLoader(), 17 | microsoft.MakeDefaultMicrosoftGraphClientLoader(client.AcquireAzureToken), 18 | microsoft.MakeDefaultMicrosoftSecurityClientLoader(client.AcquireAzureToken), 19 | ), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /internal/misp/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/misp" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | misp.Plugin(version, nil), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/misp/plugin_test.go: -------------------------------------------------------------------------------- 1 | package misp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/misp", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | } 14 | -------------------------------------------------------------------------------- /internal/nistnvd/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/nistnvd" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | nistnvd.Plugin(version, nistnvd.DefaultClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/nistnvd/plugin.go: -------------------------------------------------------------------------------- 1 | package nistnvd 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/nistnvd/client" 5 | "github.com/blackstork-io/fabric/plugin" 6 | ) 7 | 8 | type ClientLoadFn func(apiKey *string) client.Client 9 | 10 | var DefaultClientLoader ClientLoadFn = client.New 11 | 12 | func Plugin(version string, loader ClientLoadFn) *plugin.Schema { 13 | return &plugin.Schema{ 14 | Name: "blackstork/nist_nvd", 15 | Version: version, 16 | DataSources: plugin.DataSources{ 17 | "nist_nvd_cves": makeNistNvdCvesDataSource(loader), 18 | }, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/nistnvd/plugin_test.go: -------------------------------------------------------------------------------- 1 | package nistnvd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/nist_nvd", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["nist_nvd_cves"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/openai/client/options.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | type Option func(*client) 4 | 5 | func WithBaseURL(baseURL string) Option { 6 | return func(c *client) { 7 | c.baseURL = baseURL 8 | } 9 | } 10 | 11 | func WithOrgID(orgID string) Option { 12 | return func(c *client) { 13 | c.orgID = orgID 14 | } 15 | } 16 | 17 | func WithAPIKey(apiKey string) Option { 18 | return func(c *client) { 19 | c.apiKey = apiKey 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/openai/client/types.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "fmt" 4 | 5 | type ChatCompletionParams struct { 6 | Model string `json:"model"` 7 | Messages []ChatCompletionMessage `json:"messages"` 8 | } 9 | 10 | type ChatCompletionMessage struct { 11 | Role string `json:"role"` 12 | Content string `json:"content"` 13 | } 14 | 15 | type ChatCompletionResult struct { 16 | Choices []ChatCompletionChoice `json:"choices"` 17 | } 18 | 19 | type ChatCompletionChoice struct { 20 | FinishedReason string `json:"finish_reason"` 21 | Index int `json:"index"` 22 | Message ChatCompletionMessage `json:"message"` 23 | } 24 | 25 | type Error struct { 26 | Type string `json:"type"` 27 | Message string `json:"message"` 28 | } 29 | 30 | func (e Error) Error() string { 31 | return fmt.Sprintf("openai[%s]: %s", e.Type, e.Message) 32 | } 33 | 34 | type ErrorResponse struct { 35 | Error Error `json:"error"` 36 | } 37 | -------------------------------------------------------------------------------- /internal/openai/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/openai" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | openai.Plugin(version, nil), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/openai/plugin.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/openai/client" 5 | "github.com/blackstork-io/fabric/plugin" 6 | "github.com/blackstork-io/fabric/plugin/dataspec" 7 | ) 8 | 9 | const ( 10 | defaultModel = "gpt-3.5-turbo" 11 | ) 12 | 13 | type ClientLoadFn func(opts ...client.Option) client.Client 14 | 15 | var DefaultClientLoader ClientLoadFn = client.New 16 | 17 | func Plugin(version string, loader ClientLoadFn) *plugin.Schema { 18 | if loader == nil { 19 | loader = DefaultClientLoader 20 | } 21 | return &plugin.Schema{ 22 | Name: "blackstork/openai", 23 | Version: version, 24 | ContentProviders: plugin.ContentProviders{ 25 | "openai_text": makeOpenAITextContentSchema(loader), 26 | }, 27 | } 28 | } 29 | 30 | func makeClient(loader ClientLoadFn, cfg *dataspec.Block) (client.Client, error) { 31 | opts := []client.Option{ 32 | client.WithAPIKey(cfg.GetAttrVal("api_key").AsString()), 33 | } 34 | orgID := cfg.GetAttrVal("organization_id") 35 | if !orgID.IsNull() && orgID.AsString() != "" { 36 | opts = append(opts, client.WithOrgID(orgID.AsString())) 37 | } 38 | return loader(opts...), nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/openai/plugin_test.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/openai", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.ContentProviders["openai_text"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/opencti/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/opencti" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | opencti.Plugin(version), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/opencti/plugin_test.go: -------------------------------------------------------------------------------- 1 | package opencti 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3") 11 | assert.Equal(t, "blackstork/opencti", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["opencti"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/postgresql/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/postgresql" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | postgresql.Plugin(version), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/postgresql/plugin.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/plugin" 5 | ) 6 | 7 | func Plugin(version string) *plugin.Schema { 8 | return &plugin.Schema{ 9 | Name: "blackstork/postgresql", 10 | Version: version, 11 | DataSources: plugin.DataSources{ 12 | "postgresql": makePostgreSQLDataSource(), 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/postgresql/plugin_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3") 11 | assert.Equal(t, "blackstork/postgresql", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["postgresql"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/postgresql/testdata/data.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE testdata_empty ( 2 | id SERIAL PRIMARY KEY, 3 | name text 4 | ); 5 | 6 | CREATE TABLE testdata ( 7 | id SERIAL PRIMARY KEY, 8 | text_val text, 9 | int_val integer, 10 | bool_val boolean, 11 | null_val text DEFAULT NULL 12 | ); 13 | 14 | INSERT INTO testdata (text_val, int_val, bool_val) VALUES ('text_1', 1, true); 15 | INSERT INTO testdata (text_val, int_val, bool_val, null_val) VALUES ('text_2', 2, false, 'null_val_2'); -------------------------------------------------------------------------------- /internal/snyk/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/snyk" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | snyk.Plugin(version, snyk.DefaultClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/snyk/plugin.go: -------------------------------------------------------------------------------- 1 | package snyk 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/snyk/client" 5 | "github.com/blackstork-io/fabric/plugin" 6 | ) 7 | 8 | const ( 9 | pageSize = 100 10 | ) 11 | 12 | type ClientLoadFn func(apiKey string) client.Client 13 | 14 | var DefaultClientLoader ClientLoadFn = client.New 15 | 16 | func Plugin(version string, loader ClientLoadFn) *plugin.Schema { 17 | if loader == nil { 18 | loader = DefaultClientLoader 19 | } 20 | return &plugin.Schema{ 21 | Name: "blackstork/snyk", 22 | Version: version, 23 | DataSources: plugin.DataSources{ 24 | "snyk_issues": makeSnykIssuesDataSource(loader), 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/snyk/plugin_test.go: -------------------------------------------------------------------------------- 1 | package snyk 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/snyk", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["snyk_issues"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/splunk/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/splunk" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | splunk.Plugin(version, splunk.DefaultClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/splunk/plugin.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | 8 | "github.com/blackstork-io/fabric/internal/splunk/client" 9 | "github.com/blackstork-io/fabric/plugin" 10 | "github.com/blackstork-io/fabric/plugin/dataspec" 11 | ) 12 | 13 | type ClientLoadFn func(token, host, deployment string) client.Client 14 | 15 | var DefaultClientLoader ClientLoadFn = client.New 16 | 17 | func Plugin(version string, loader ClientLoadFn) *plugin.Schema { 18 | if loader == nil { 19 | loader = DefaultClientLoader 20 | } 21 | return &plugin.Schema{ 22 | Name: "blackstork/splunk", 23 | Version: version, 24 | DataSources: plugin.DataSources{ 25 | "splunk_search": makeSplunkSearchDataSchema(loader), 26 | }, 27 | } 28 | } 29 | 30 | func makeClient(loader ClientLoadFn, cfg *dataspec.Block) (client.Client, error) { 31 | if cfg == nil { 32 | return nil, fmt.Errorf("configuration is required") 33 | } 34 | 35 | token := cfg.GetAttrVal("auth_token") 36 | if token.IsNull() || token.AsString() == "" { 37 | return nil, fmt.Errorf("auth_token is required in configuration") 38 | } 39 | host := cfg.GetAttrVal("host") 40 | if host.IsNull() { 41 | host = cty.StringVal("") 42 | } 43 | deployment := cfg.GetAttrVal("deployment_name") 44 | if deployment.IsNull() { 45 | deployment = cty.StringVal("") 46 | } 47 | if host.AsString() == "" && deployment.AsString() == "" { 48 | return nil, fmt.Errorf("host or deployment_name is required in configuration") 49 | } 50 | return loader(token.AsString(), host.AsString(), deployment.AsString()), nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/splunk/plugin_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/splunk", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["splunk_search"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/sqlite/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/sqlite" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | sqlite.Plugin(version), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/sqlite/plugin.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/plugin" 5 | ) 6 | 7 | func Plugin(version string) *plugin.Schema { 8 | return &plugin.Schema{ 9 | Name: "blackstork/sqlite", 10 | Version: version, 11 | DataSources: plugin.DataSources{ 12 | "sqlite": makeSqliteDataSource(), 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/sqlite/plugin_test.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3") 11 | assert.Equal(t, "blackstork/sqlite", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["sqlite"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/stixview/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/stixview" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | stixview.Plugin(version), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/stixview/plugin.go: -------------------------------------------------------------------------------- 1 | package stixview 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/plugin" 5 | ) 6 | 7 | func Plugin(version string) *plugin.Schema { 8 | return &plugin.Schema{ 9 | Name: "blackstork/stixview", 10 | Version: version, 11 | ContentProviders: plugin.ContentProviders{ 12 | "stixview": makeStixViewContentProvider(), 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/stixview/plugin_test.go: -------------------------------------------------------------------------------- 1 | package stixview 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3") 11 | assert.Equal(t, "blackstork/stixview", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.ContentProviders["stixview"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/terraform/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/terraform" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | terraform.Plugin(version), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/terraform/plugin.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/plugin" 5 | ) 6 | 7 | func Plugin(version string) *plugin.Schema { 8 | return &plugin.Schema{ 9 | Name: "blackstork/terraform", 10 | Version: version, 11 | DataSources: plugin.DataSources{ 12 | "terraform_state_local": makeTerraformStateLocalDataSource(), 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/terraform/plugin_test.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3") 11 | assert.Equal(t, "blackstork/terraform", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["terraform_state_local"]) 14 | } 15 | -------------------------------------------------------------------------------- /internal/terraform/testdata/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "serial": 0, 4 | "modules": [ 5 | { 6 | "path": ["root"], 7 | "outputs": {}, 8 | "resources": {} 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /internal/virustotal/client/dto.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "time" 7 | ) 8 | 9 | type GetUserAPIUsageReq struct { 10 | User string `url:"-"` 11 | StartDate *Date `url:"start_date,omitempty"` 12 | EndDate *Date `url:"end_date,omitempty"` 13 | } 14 | 15 | type GetGroupAPIUsageReq struct { 16 | Group string `url:"-"` 17 | StartDate *Date `url:"start_date,omitempty"` 18 | EndDate *Date `url:"end_date,omitempty"` 19 | } 20 | 21 | type Error struct { 22 | Code string `json:"code"` 23 | Message string `json:"message"` 24 | } 25 | 26 | func (e Error) Error() string { 27 | return fmt.Sprintf("%s: %s", e.Code, e.Message) 28 | } 29 | 30 | type ErrorRes struct { 31 | Error Error `json:"error"` 32 | } 33 | 34 | type GetUserAPIUsageRes struct { 35 | Data map[string]any `json:"data"` 36 | } 37 | 38 | type GetGroupAPIUsageRes struct { 39 | Data map[string]any `json:"data"` 40 | } 41 | 42 | type Date struct { 43 | time.Time 44 | } 45 | 46 | func (d Date) EncodeValues(key string, v *url.Values) error { 47 | v.Add(key, d.Time.Format("20060102")) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/virustotal/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/virustotal" 5 | pluginapiv1 "github.com/blackstork-io/fabric/plugin/pluginapi/v1" 6 | ) 7 | 8 | var version string 9 | 10 | func main() { 11 | pluginapiv1.Serve( 12 | virustotal.Plugin(version, virustotal.DefaultClientLoader), 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /internal/virustotal/plugin.go: -------------------------------------------------------------------------------- 1 | package virustotal 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/internal/virustotal/client" 5 | "github.com/blackstork-io/fabric/plugin" 6 | ) 7 | 8 | type ClientLoadFn func(key string) client.Client 9 | 10 | var DefaultClientLoader ClientLoadFn = client.New 11 | 12 | func Plugin(version string, loader ClientLoadFn) *plugin.Schema { 13 | if loader == nil { 14 | loader = DefaultClientLoader 15 | } 16 | return &plugin.Schema{ 17 | Name: "blackstork/virustotal", 18 | Version: version, 19 | DataSources: plugin.DataSources{ 20 | "virustotal_api_usage": makeVirusTotalAPIUsageDataSchema(loader), 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/virustotal/plugin_test.go: -------------------------------------------------------------------------------- 1 | package virustotal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPlugin_Schema(t *testing.T) { 10 | schema := Plugin("1.2.3", nil) 11 | assert.Equal(t, "blackstork/virustotal", schema.Name) 12 | assert.Equal(t, "1.2.3", schema.Version) 13 | assert.NotNil(t, schema.DataSources["virustotal_api_usage"]) 14 | } 15 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: build test-run 2 | 3 | build: 4 | goreleaser build --config ./.goreleaser-dev.yaml --single-target --snapshot --clean 5 | 6 | test-run: 7 | ./dist/fabric render "document.hello" --source-dir ./examples/templates/basic_hello/ -v 8 | 9 | format: 10 | go mod tidy 11 | ./codegen/format.sh 12 | 13 | format-extra: format 14 | gofumpt -w -extra . 15 | 16 | lint: format 17 | golangci-lint run 18 | 19 | test: 20 | go test -timeout 10s -race -short -v ./... 21 | 22 | test-pretty: 23 | gotestsum --format dots-v2 -- -timeout 10s -race -short -v ./... 24 | 25 | test-all: 26 | go test -timeout 5m -race -v ./... 27 | 28 | generate: 29 | ./codegen/gen_code.sh 30 | 31 | generate-docs: 32 | ./codegen/gen_docs.sh 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blackstork-io/fabric/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /parser/defined_blocks_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/hcl/v2/hclsyntax" 7 | "github.com/stretchr/testify/assert" 8 | 9 | definitions_mocks "github.com/blackstork-io/fabric/mocks/parser/definitions" 10 | "github.com/blackstork-io/fabric/parser" 11 | ) 12 | 13 | func TestAddIfMissing(t *testing.T) { 14 | t.Parallel() 15 | assert := assert.New(t) 16 | m := map[string]*definitions_mocks.FabricBlock{} 17 | 18 | m1 := definitions_mocks.NewFabricBlock(t) 19 | m1.EXPECT().GetHCLBlock().Return(&hclsyntax.Block{}) 20 | 21 | diag := parser.AddIfMissing(m, "key_1", m1) 22 | assert.Empty(diag) 23 | assert.Same(m1, m["key_1"]) 24 | 25 | m2 := definitions_mocks.NewFabricBlock(t) 26 | 27 | diag = parser.AddIfMissing(m, "key_2", m2) 28 | assert.Empty(diag) 29 | assert.Same(m1, m["key_1"]) 30 | assert.Same(m2, m["key_2"]) 31 | 32 | m3 := definitions_mocks.NewFabricBlock(t) 33 | m3.EXPECT().GetHCLBlock().Return(&hclsyntax.Block{}).Once() 34 | 35 | diag = parser.AddIfMissing(m, "key_1", m3) 36 | assert.NotEmpty(diag) 37 | assert.Same(m1, m["key_1"]) 38 | assert.Same(m2, m["key_2"]) 39 | } 40 | -------------------------------------------------------------------------------- /parser/definitions/config_ptr.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | 8 | "github.com/blackstork-io/fabric/parser/evaluation" 9 | "github.com/blackstork-io/fabric/pkg/diagnostics" 10 | "github.com/blackstork-io/fabric/plugin/dataspec" 11 | ) 12 | 13 | // Attribute referencing a configuration block (`config = path.to.config`). 14 | type ConfigPtr struct { 15 | Cfg *Config 16 | Ptr *hcl.Attribute 17 | } 18 | 19 | // Exists implements evaluation.Configuration. 20 | func (c *ConfigPtr) Exists() bool { 21 | return c != nil 22 | } 23 | 24 | // ParseConfig implements Configuration. 25 | func (c *ConfigPtr) ParseConfig(ctx context.Context, spec *dataspec.RootSpec) (val *dataspec.Block, diags diagnostics.Diag) { 26 | return c.Cfg.ParseConfig(ctx, spec) 27 | } 28 | 29 | // Range implements Configuration. 30 | func (c *ConfigPtr) Range() hcl.Range { 31 | // Use the location of "config = *traversal*" for error reporting, not original config's Range 32 | return c.Ptr.Range 33 | } 34 | 35 | var _ evaluation.Configuration = (*ConfigPtr)(nil) 36 | -------------------------------------------------------------------------------- /parser/definitions/definitions.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2/hclsyntax" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | const ( 9 | BlockKindDocument = "document" 10 | BlockKindConfig = "config" 11 | BlockKindContent = "content" 12 | BlockKindPublish = "publish" 13 | BlockKindData = "data" 14 | BlockKindMeta = "meta" 15 | BlockKindVars = "vars" 16 | BlockKindSection = "section" 17 | BlockKindGlobalConfig = "fabric" 18 | BlockKindDynamic = "dynamic" 19 | 20 | PluginTypeRef = "ref" 21 | AttrRefBase = "base" 22 | AttrTitle = "title" 23 | AttrDependsOn = "depends_on" 24 | AttrLocalVar = "local_var" 25 | AttrRequiredVars = "required_vars" 26 | AttrIsIncluded = "is_included" 27 | AttrDynamicItems = "items" 28 | ) 29 | 30 | type FabricBlock interface { 31 | GetHCLBlock() *hclsyntax.Block 32 | CtyType() cty.Type 33 | } 34 | 35 | func ToCtyValue(b FabricBlock) cty.Value { 36 | return cty.CapsuleVal(b.CtyType(), b) 37 | } 38 | 39 | // Identifies a plugin block 40 | type Key struct { 41 | PluginKind string 42 | PluginName string 43 | BlockName string 44 | } 45 | -------------------------------------------------------------------------------- /parser/definitions/document.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/hashicorp/hcl/v2/hclsyntax" 6 | "github.com/zclconf/go-cty/cty" 7 | 8 | "github.com/blackstork-io/fabric/pkg/diagnostics" 9 | "github.com/blackstork-io/fabric/pkg/encapsulator" 10 | ) 11 | 12 | type Document struct { 13 | Block *hclsyntax.Block 14 | Name string 15 | Meta *MetaBlock 16 | } 17 | 18 | var _ FabricBlock = (*Document)(nil) 19 | 20 | func (d *Document) GetHCLBlock() *hclsyntax.Block { 21 | return d.Block 22 | } 23 | 24 | var ctyDocumentType = encapsulator.NewEncoder[Document]("document", nil) 25 | 26 | func (d *Document) CtyType() cty.Type { 27 | return ctyDocumentType.CtyType() 28 | } 29 | 30 | func DefineDocument(block *hclsyntax.Block) (doc *Document, diags diagnostics.Diag) { 31 | diags.Append(validateBlockName(block, 0, true)) 32 | diags.Append(validateLabelsLength(block, 1, "document_name")) 33 | if diags.HasErrors() { 34 | return 35 | } 36 | 37 | if block.Labels[0] == AttrRefBase { 38 | diags.Append(&hcl.Diagnostic{ 39 | Severity: hcl.DiagError, 40 | Summary: "Invalid document declaration", 41 | Detail: "Documents can't be refs, only sections can", 42 | Subject: &block.LabelRanges[0], 43 | Context: block.DefRange().Ptr(), 44 | }) 45 | } 46 | 47 | doc = &Document{ 48 | Block: block, 49 | Name: block.Labels[0], 50 | } 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /parser/definitions/dynamic.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2/hclsyntax" 5 | ) 6 | 7 | type Dynamic struct { 8 | ParseResult *ParsedDynamic 9 | } 10 | 11 | type ParsedDynamic struct { 12 | Block *hclsyntax.Block 13 | // Items is a list of items to be iterated over dynamically. 14 | // Always present. 15 | Items *hclsyntax.Attribute 16 | Content []*ParsedContent 17 | } 18 | -------------------------------------------------------------------------------- /parser/definitions/meta.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | import ( 4 | "slices" 5 | 6 | "github.com/blackstork-io/fabric/plugin/plugindata" 7 | ) 8 | 9 | type MetaBlock struct { 10 | Name string `hcl:"name,optional"` 11 | Description string `hcl:"description,optional"` 12 | Url string `hcl:"url,optional"` 13 | License string `hcl:"license,optional"` 14 | Authors []string `hcl:"authors,optional"` 15 | Tags []string `hcl:"tags,optional"` 16 | UpdatedAt string `hcl:"updated_at,optional"` 17 | Version string `hcl:"version,optional"` 18 | 19 | // TODO: ?store def range defRange hcl.Range 20 | } 21 | 22 | func (m *MetaBlock) MatchesTags(requiredTags []string) bool { 23 | var tags []string 24 | if m != nil { 25 | tags = m.Tags 26 | } 27 | if len(tags) < len(requiredTags) { 28 | return false 29 | } 30 | for _, tag := range requiredTags { 31 | if !slices.Contains(tags, tag) { 32 | return false 33 | } 34 | } 35 | return true 36 | } 37 | 38 | func (m *MetaBlock) AsPluginData() plugindata.Data { 39 | tags := make(plugindata.List, len(m.Tags)) 40 | authors := make(plugindata.List, len(m.Authors)) 41 | for i, tag := range m.Tags { 42 | tags[i] = plugindata.String(tag) 43 | } 44 | for i, author := range m.Authors { 45 | authors[i] = plugindata.String(author) 46 | } 47 | return plugindata.Map{ 48 | "authors": authors, 49 | "name": plugindata.String(m.Name), 50 | "tags": tags, 51 | "version": plugindata.String(m.Version), 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /parser/definitions/parsed_document.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | type ParsedDocument struct { 4 | Source *Document 5 | Meta *MetaBlock 6 | Vars *ParsedVars 7 | RequiredVars []string 8 | Content []*ParsedContent 9 | Data []*ParsedPlugin 10 | Publish []*ParsedPlugin 11 | } 12 | -------------------------------------------------------------------------------- /parser/definitions/parsed_plugin.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2/hclsyntax" 5 | 6 | "github.com/blackstork-io/fabric/parser/evaluation" 7 | ) 8 | 9 | type ParsedPlugin struct { 10 | Source *Plugin 11 | PluginName string 12 | BlockName string 13 | Meta *MetaBlock 14 | Config evaluation.Configuration 15 | Invocation *evaluation.BlockInvocation 16 | Vars *ParsedVars 17 | RequiredVars []string 18 | DependsOn []string 19 | IsIncluded *hclsyntax.Attribute 20 | } 21 | 22 | type ParsedContent struct { 23 | Section *ParsedSection 24 | Plugin *ParsedPlugin 25 | Dynamic *ParsedDynamic 26 | } 27 | -------------------------------------------------------------------------------- /parser/definitions/vars.go: -------------------------------------------------------------------------------- 1 | package definitions 2 | 3 | import ( 4 | "maps" 5 | "slices" 6 | 7 | "github.com/blackstork-io/fabric/plugin/dataspec" 8 | ) 9 | 10 | const LocalVarName = "local" 11 | 12 | type ParsedVars struct { 13 | // stored in the order of definition 14 | Variables []*dataspec.Attr 15 | ByName map[string]int 16 | } 17 | 18 | func (pv *ParsedVars) Empty() bool { 19 | return pv == nil || len(pv.Variables) == 0 20 | } 21 | 22 | // MergeWithBaseVars handles merging with vars from ref base. 23 | // Shadowing has different rules, and will be handled at the evaluation stage. 24 | func (pv *ParsedVars) MergeWithBaseVars(baseVars *ParsedVars) *ParsedVars { 25 | if pv.Empty() { 26 | return baseVars 27 | } 28 | if baseVars.Empty() { 29 | return pv 30 | } 31 | 32 | vars := slices.Clone(baseVars.Variables) 33 | byName := maps.Clone(baseVars.ByName) 34 | for _, v := range pv.Variables { 35 | if idx, found := byName[v.Name]; found { 36 | // redefine, but keep the definition order 37 | vars[idx] = v 38 | } else { 39 | byName[v.Name] = len(vars) 40 | vars = append(vars, v) 41 | } 42 | } 43 | return &ParsedVars{ 44 | Variables: vars, 45 | ByName: byName, 46 | } 47 | } 48 | 49 | // AppendVar append a variable to the parsed vars struct (last in evaluation order). 50 | func (pv *ParsedVars) AppendVar(variable *dataspec.Attr) { 51 | idx := len(pv.Variables) 52 | pv.Variables = append(pv.Variables, variable) 53 | if idx == 0 { 54 | pv.ByName = make(map[string]int) 55 | } 56 | pv.ByName[variable.Name] = idx 57 | } 58 | -------------------------------------------------------------------------------- /parser/evaluation/block_invocation.go: -------------------------------------------------------------------------------- 1 | package evaluation 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2/hclsyntax" 5 | ) 6 | 7 | type BlockInvocation struct { 8 | *hclsyntax.Block 9 | } 10 | -------------------------------------------------------------------------------- /parser/evaluation/evaluation.go: -------------------------------------------------------------------------------- 1 | package evaluation 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | 8 | "github.com/blackstork-io/fabric/pkg/diagnostics" 9 | "github.com/blackstork-io/fabric/plugin/dataspec" 10 | ) 11 | 12 | // To act as a plugin configuration struct must implement this interface. 13 | type Configuration interface { 14 | ParseConfig(ctx context.Context, spec *dataspec.RootSpec) (*dataspec.Block, diagnostics.Diag) 15 | Range() hcl.Range 16 | Exists() bool 17 | } 18 | -------------------------------------------------------------------------------- /parser/parse_title.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/hashicorp/hcl/v2/hclsyntax" 8 | "github.com/zclconf/go-cty/cty" 9 | 10 | "github.com/blackstork-io/fabric/parser/definitions" 11 | "github.com/blackstork-io/fabric/pkg/diagnostics" 12 | "github.com/blackstork-io/fabric/pkg/utils" 13 | ) 14 | 15 | func (db *DefinedBlocks) ParseTitle(ctx context.Context, title *hclsyntax.Attribute) (res *definitions.ParsedContent, diags diagnostics.Diag) { 16 | const pluginName = "title" 17 | 18 | value := *title 19 | value.Name = "value" 20 | 21 | relativeSize := *title 22 | relativeSize.Name = "relative_size" 23 | relativeSize.Expr = &hclsyntax.LiteralValueExpr{ 24 | Val: cty.NumberIntVal(-1), 25 | SrcRange: title.Expr.Range(), 26 | } 27 | 28 | block := &hclsyntax.Block{ 29 | Type: definitions.BlockKindContent, 30 | TypeRange: title.NameRange, 31 | Labels: []string{pluginName}, 32 | LabelRanges: []hcl.Range{title.NameRange}, 33 | Body: &hclsyntax.Body{ 34 | Attributes: hclsyntax.Attributes{ 35 | "value": &value, 36 | "relative_size": &relativeSize, 37 | }, 38 | SrcRange: title.SrcRange, 39 | EndRange: utils.RangeEnd(title.Expr.Range()), 40 | }, 41 | OpenBraceRange: utils.RangeStart(title.NameRange), 42 | CloseBraceRange: utils.RangeEnd(title.Expr.Range()), 43 | } 44 | def, diag := definitions.DefinePlugin(block, false) 45 | if diags.Extend(diag) { 46 | return 47 | } 48 | parsed, diag := db.ParsePlugin(ctx, def) 49 | if diags.Extend(diag) { 50 | return 51 | } 52 | res = &definitions.ParsedContent{ 53 | Plugin: parsed, 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "testing" 5 | "testing/fstest" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/blackstork-io/fabric/parser" 10 | ) 11 | 12 | func TestFindFiles(t *testing.T) { 13 | t.Parallel() 14 | assert := assert.New(t) 15 | fs := fstest.MapFS{ 16 | "f1.fabric": &fstest.MapFile{}, 17 | "f2.fAbRiC": &fstest.MapFile{}, 18 | "f3.not_fabric": &fstest.MapFile{}, 19 | "subdir/f4.fAbRiC": &fstest.MapFile{}, 20 | "subdir/f5.not_fabric": &fstest.MapFile{}, 21 | } 22 | 23 | type testCase struct { 24 | name string 25 | recursive bool 26 | expected []string 27 | } 28 | 29 | testCases := []testCase{ 30 | { 31 | name: "Recursive", 32 | recursive: true, 33 | expected: []string{ 34 | "f1.fabric", 35 | "f2.fAbRiC", 36 | "subdir/f4.fAbRiC", 37 | }, 38 | }, 39 | { 40 | name: "Non-recursive", 41 | recursive: false, 42 | expected: []string{ 43 | "f1.fabric", 44 | "f2.fAbRiC", 45 | }, 46 | }, 47 | } 48 | for _, tc := range testCases { 49 | tc := tc 50 | 51 | t.Run(tc.name, func(t *testing.T) { 52 | t.Parallel() 53 | var res []string 54 | 55 | diags := parser.FindFabricFiles(fs, tc.recursive, func(path string) { 56 | res = append(res, path) 57 | }) 58 | 59 | assert.Equal( 60 | tc.expected, 61 | res, 62 | ) 63 | assert.Empty(diags) 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /parser/traversal.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/zclconf/go-cty/cty" 8 | 9 | "github.com/blackstork-io/fabric/parser/definitions" 10 | "github.com/blackstork-io/fabric/pkg/diagnostics" 11 | ) 12 | 13 | func Resolve[B definitions.FabricBlock](db *DefinedBlocks, expr hcl.Expression) (res B, diags diagnostics.Diag) { 14 | resAny, diags := db.resolve(expr, res.CtyType()) 15 | if diags.HasErrors() { 16 | return 17 | } 18 | res = resAny.(B) //nolint:forcetypeassert // This type assertion is done via cty in db.resolve 19 | return 20 | } 21 | 22 | func (db *DefinedBlocks) resolve(expr hcl.Expression, expectedType cty.Type) (res any, diags diagnostics.Diag) { 23 | val, diag := expr.Value(&hcl.EvalContext{ 24 | Variables: db.AsValueMap(), 25 | }) 26 | if diags.Extend(diag) { 27 | return 28 | } 29 | ty := val.Type() 30 | if !ty.Equals(expectedType) { 31 | diags.Append(&hcl.Diagnostic{ 32 | Severity: hcl.DiagError, 33 | Summary: "Incorrect reference", 34 | Detail: fmt.Sprintf( 35 | "Expected reference to '%s' got reference to '%s'", 36 | expectedType.FriendlyName(), 37 | ty.FriendlyName(), 38 | ), 39 | Subject: expr.Range().Ptr(), 40 | }) 41 | return 42 | } 43 | res = val.EncapsulatedValue() 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /pkg/diagnostics/extra.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import "github.com/hashicorp/hcl/v2" 4 | 5 | type extraList []any 6 | 7 | func getExtra[T any](extra any) (_ T, _ bool) { 8 | for extra != nil { 9 | if list, ok := extra.(extraList); ok { 10 | for _, extra := range list { 11 | val, found := getExtra[T](extra) 12 | if found { 13 | return val, found 14 | } 15 | } 16 | return 17 | } 18 | if val, ok := extra.(T); ok { 19 | return val, true 20 | } 21 | if val, ok := extra.(hcl.DiagnosticExtraUnwrapper); ok { 22 | extra = val.UnwrapDiagnosticExtra() 23 | continue 24 | } 25 | return 26 | } 27 | return 28 | } 29 | 30 | // GetExtra finds extra of type T in a provided diagnostic 31 | func GetExtra[T any](diag *hcl.Diagnostic) (extra T, found bool) { 32 | return getExtra[T](diag.Extra) 33 | } 34 | 35 | // DiagnosticsGetExtra finds the first extra of type T in the slice of provided diagnostics 36 | func DiagnosticsGetExtra[T any](diags Diag) (extra T, found bool) { 37 | for _, diag := range diags { 38 | if extra, found = getExtra[T](diag.Extra); found { 39 | return 40 | } 41 | } 42 | return 43 | } 44 | 45 | func FindByExtra[T any](diags Diag) *hcl.Diagnostic { 46 | for _, diag := range diags { 47 | if _, found := getExtra[T](diag.Extra); found { 48 | return diag 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | // Adds extra without replacing existing extras. 55 | func addExtraFunc(diag *hcl.Diagnostic, extra any) { 56 | switch extraT := diag.Extra.(type) { 57 | case nil: 58 | diag.Extra = extra 59 | case extraList: 60 | extraT = append(extraT, extra) 61 | diag.Extra = extraT 62 | default: 63 | diag.Extra = extraList{extraT, extra} 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/diagnostics/printer.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "log/slog" 7 | "os" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | "github.com/mattn/go-colorable" 11 | "golang.org/x/term" 12 | ) 13 | 14 | func PrintDiags(output io.Writer, diags []*hcl.Diagnostic, fileMap map[string]*hcl.File, colorize bool) { 15 | if len(diags) == 0 { 16 | return 17 | } 18 | width := 80 19 | if file, ok := output.(*os.File); ok { 20 | termWidth, _, err := term.GetSize(int(file.Fd())) 21 | if err == nil && termWidth > 0 { 22 | width = termWidth 23 | } 24 | if colorize { 25 | output = colorable.NewColorable(file) 26 | } 27 | } 28 | 29 | bufWr := bufio.NewWriter(output) 30 | defer func() { 31 | err := bufWr.Flush() 32 | if err != nil { 33 | slog.Error("Failed to flush diagnostics", "err", err) 34 | } 35 | }() 36 | 37 | // Convert width to uint safely to avoid potential overflow 38 | var diagWidth uint 39 | if width > 0 { 40 | diagWidth = uint(width) 41 | } else { 42 | diagWidth = 80 // Default width 43 | } 44 | 45 | diagWriter := hcl.NewDiagnosticTextWriter(bufWr, fileMap, diagWidth, colorize) 46 | 47 | for _, diag := range diags { 48 | if _, isRepeated := GetExtra[repeatedError](diag); isRepeated { 49 | continue 50 | } 51 | if gojqErr, ok := GetExtra[GoJQError](diag); ok { 52 | gojqErr.improveDiagnostic(diag, fileMap) 53 | } 54 | if traceback, ok := GetExtra[TracebackExtra](diag); ok { 55 | traceback.improveDiagnostic(diag) 56 | } 57 | if path, ok := GetExtra[PathExtra](diag); ok { 58 | path.improveDiagnostic(diag) 59 | } 60 | err := diagWriter.WriteDiagnostic(diag) 61 | if err != nil { 62 | slog.Error("Failed to write diagnostics", "err", err) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/diagnostics/refine.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | type Refiner interface { 9 | Refine(diags Diag) 10 | } 11 | 12 | // Set Summary field if empty 13 | type DefaultSummary string 14 | 15 | func (ds DefaultSummary) Refine(diags Diag) { 16 | for _, d := range diags { 17 | if d.Summary == "" { 18 | d.Summary = string(ds) 19 | } 20 | } 21 | } 22 | 23 | // Set Subject field if empty 24 | type DefaultSubject hcl.Range 25 | 26 | func (ds DefaultSubject) Refine(diags Diag) { 27 | for _, d := range diags { 28 | if d.Subject == nil { 29 | d.Subject = (*hcl.Range)(&ds) 30 | } 31 | } 32 | } 33 | 34 | // Adds an extra without replacing existing extras. 35 | func AddExtra(extra any) Refiner { 36 | switch eT := extra.(type) { 37 | case *PathExtra: 38 | return eT 39 | case cty.Path: 40 | return AddPath(eT) 41 | default: 42 | return &extraAdder{eT} 43 | } 44 | } 45 | 46 | type extraAdder struct { 47 | extra any 48 | } 49 | 50 | func (ae *extraAdder) Refine(diags Diag) { 51 | for _, d := range diags { 52 | addExtraFunc(d, ae.extra) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/diagnostics/repeated.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import "github.com/hashicorp/hcl/v2" 4 | 5 | type repeatedError struct{} 6 | 7 | // Invisible to user error, typically used to signal that the initial block evaluation 8 | // has failed (and already has reported its errors to user). 9 | var RepeatedError = &hcl.Diagnostic{ 10 | Severity: hcl.DiagError, 11 | Extra: repeatedError{}, 12 | } 13 | -------------------------------------------------------------------------------- /pkg/diagnostics/traceback.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | ) 8 | 9 | type TracebackExtra = *tracebackExtra 10 | 11 | type tracebackExtra struct { 12 | Traceback []*hcl.Range 13 | } 14 | 15 | func (tb *tracebackExtra) improveDiagnostic(diag *hcl.Diagnostic) { 16 | sb := []byte(diag.Detail) 17 | for _, rng := range tb.Traceback { 18 | if rng != nil { 19 | sb = fmt.Appendf(sb, "\n at %s:%d:%d", 20 | rng.Filename, rng.Start.Line, rng.Start.Column, 21 | ) 22 | } else { 23 | sb = append(sb, "\n at "...) 24 | } 25 | } 26 | diag.Detail = string(sb) 27 | } 28 | 29 | func NewTracebackExtra() TracebackExtra { 30 | return &tracebackExtra{} 31 | } 32 | -------------------------------------------------------------------------------- /pkg/encapsulator/codec.go: -------------------------------------------------------------------------------- 1 | package encapsulator 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // Codec is both an Encoder and a Decoder for *T. 8 | type Codec[T any] struct { 9 | Decoder[*T] 10 | Encoder[T] 11 | } 12 | 13 | // NewCodec creates a Codec (encoder + decoder). 14 | // Will use cty.Capsule if capsuleOps is nil, and cty.CapsuleWithOps otherwise 15 | func NewCodec[T any](friendlyName string, capsuleOps *CapsuleOps[T]) *Codec[T] { 16 | goType := reflect.TypeFor[T]() 17 | codec := &Codec[T]{ 18 | Encoder: Encoder[T]{}, 19 | Decoder: Decoder[*T]{ 20 | decoderCore: decoderCore{ 21 | goType: reflect.PointerTo(goType), 22 | }, 23 | }, 24 | } 25 | codec.initEncoderCore(friendlyName, goType, capsuleOps) 26 | return codec 27 | } 28 | -------------------------------------------------------------------------------- /pkg/encapsulator/util.go: -------------------------------------------------------------------------------- 1 | // Type-safe conversions to and from cty.CapsuleValue 2 | package encapsulator 3 | 4 | import ( 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | // isValid tests that cty.Value is safe to work with. 9 | func isValid(v cty.Value) bool { 10 | return !v.IsNull() && v.IsKnown() 11 | } 12 | 13 | // Compatible checks that values produced by the given Encoder are decodable by the given Decoder. 14 | // This is a loose check, for example it allows decoding value that implements interface DT. 15 | func Compatible(encoder EncoderI, decoder DecoderI) bool { 16 | return decoder.Decodable(encoder.CtyType()) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/jsontools/jsontools.go: -------------------------------------------------------------------------------- 1 | package jsontools 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | var ( 10 | ErrTraversal = errors.New("traversal failed") 11 | ErrUnmarshalBytes = errors.New("expected a slice of bytes") 12 | ) 13 | 14 | func MapSet(m map[string]any, keys []string, val any) (map[string]any, error) { 15 | curMap := m 16 | 17 | if len(keys) == 0 { 18 | return m, ErrTraversal 19 | } 20 | 21 | for _, k := range keys[:len(keys)-1] { 22 | v, found := curMap[k] 23 | if found { 24 | var ok bool 25 | if curMap, ok = v.(map[string]any); !ok { 26 | return m, ErrTraversal 27 | } 28 | } else { 29 | nextMap := map[string]any{} 30 | curMap[k] = nextMap 31 | curMap = nextMap 32 | } 33 | } 34 | 35 | curMap[keys[len(keys)-1]] = val 36 | return m, nil 37 | } 38 | 39 | func MapGet(m any, keys []string) (val any, err error) { 40 | if len(keys) == 0 { 41 | err = ErrTraversal 42 | return 43 | } 44 | val = m 45 | for _, k := range keys { 46 | asMap, ok := val.(map[string]any) 47 | if !ok { 48 | err = ErrTraversal 49 | return 50 | } 51 | val, ok = asMap[k] 52 | if !ok { 53 | err = ErrTraversal 54 | return 55 | } 56 | } 57 | return 58 | } 59 | 60 | func Dump(obj any) string { 61 | objBytes, err := json.Marshal(obj) 62 | if err != nil { 63 | objBytes = []byte(fmt.Sprintf("Failed to dump the object as json: %s", err)) 64 | } 65 | return string(objBytes) 66 | } 67 | 68 | func UnmarshalBytes(bytes, value any) error { 69 | data, ok := bytes.([]byte) 70 | if !ok { 71 | return ErrUnmarshalBytes 72 | } 73 | return json.Unmarshal(data, value) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/parexec/limiter.go: -------------------------------------------------------------------------------- 1 | package parexec 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | ) 7 | 8 | // NoLimit doesn't limit the degree of parallel execution. 9 | var NoLimit = (*Limiter)(nil) 10 | 11 | // DiskIOLimiter is a shared limiter for all Disk IO tasks. 12 | var DiskIOLimiter = NewLimiter(4) 13 | 14 | // CPULimiter is a shared limiter for all CPU-bound tasks. 15 | var CPULimiter = NewLimiter(2 * runtime.NumCPU()) 16 | 17 | // 4 -> 4+2 18 | // 8 -> 8+3 19 | // 16 -> 16+4 20 | 21 | type Limiter struct { 22 | cond sync.Cond 23 | available int 24 | total int 25 | } 26 | 27 | // Limits parallel executions to at most limit simultaneously. 28 | // 29 | // Can be shared between multiple [ParExecutor]s. 30 | func NewLimiter(limit int) *Limiter { 31 | l := &Limiter{ 32 | available: limit, 33 | total: limit, 34 | } 35 | l.cond = *sync.NewCond(&sync.Mutex{}) 36 | return l 37 | } 38 | 39 | // Takes a limiter token. Must [Return] it after. 40 | func (l *Limiter) Take() { 41 | l.cond.L.Lock() 42 | for l.available <= 0 { 43 | l.cond.Wait() 44 | } 45 | l.available-- 46 | l.cond.L.Unlock() 47 | } 48 | 49 | // Returns a token taken with Take. 50 | func (l *Limiter) Return() { 51 | l.cond.L.Lock() 52 | l.available++ 53 | l.cond.L.Unlock() 54 | l.cond.Signal() 55 | } 56 | -------------------------------------------------------------------------------- /pkg/parexec/limiter_test.go: -------------------------------------------------------------------------------- 1 | package parexec 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLimiterOnPanicingJob(t *testing.T) { 10 | defer func() { 11 | if r := recover(); r == nil { 12 | t.Log("panic caught!", r) 13 | } 14 | }() 15 | 16 | t.Parallel() 17 | assert := assert.New(t) 18 | 19 | limiter := NewLimiter(4) 20 | 21 | var resArr []int 22 | 23 | pe := New(limiter, func(res, idx int) (cmd Command) { 24 | resArr = append(resArr, res) 25 | return 26 | }) 27 | 28 | for i := 0; i < 8; i++ { 29 | pe.Go(func() int { 30 | panic("panicErr") 31 | // return 0 32 | }) 33 | } 34 | 35 | wasStopped := pe.WaitDoneAndLock() 36 | assert.False(wasStopped) 37 | 38 | limiter.cond.L.Lock() 39 | assert.Equal(limiter.total, limiter.available) 40 | limiter.cond.L.Unlock() 41 | 42 | pe.UnlockResume() 43 | // check that everything is still usable after panic in the executor 44 | for i := 0; i < 8; i++ { 45 | pe.Go(func() int { 46 | return 1 47 | }) 48 | } 49 | 50 | wasStopped = pe.WaitDoneAndLock() 51 | 52 | assert.False(wasStopped) 53 | limiter.cond.L.Lock() 54 | assert.Equal(limiter.total, limiter.available) 55 | limiter.cond.L.Unlock() 56 | 57 | assert.Len(resArr, 8) 58 | 59 | for _, val := range resArr { 60 | assert.Equal(1, val) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/utils/golang.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // Correct version of nil check, works on nil interfaces as well as any other value. 8 | func IsNil(val any) bool { 9 | // Checking for nil on interface objects is terrible 10 | // Thanks to: https://stackoverflow.com/a/76595928/4632951 11 | if val == nil { 12 | return true 13 | } 14 | v := reflect.ValueOf(val) 15 | k := v.Kind() 16 | switch k { 17 | case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, 18 | reflect.UnsafePointer, reflect.Interface, reflect.Slice: 19 | return v.IsNil() 20 | } 21 | 22 | return false 23 | } 24 | 25 | // Unwraps the value, panics if err is not nil. 26 | func Must[T any](val T, err error) T { 27 | if err != nil { 28 | panic(err) 29 | } 30 | return val 31 | } 32 | 33 | // Clone performs a shallow clone operation on pointer value 34 | func Clone[T any](val *T) *T { 35 | if val == nil { 36 | return nil 37 | } 38 | valC := *val 39 | return &valC 40 | } 41 | -------------------------------------------------------------------------------- /pkg/utils/maps_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPop(t *testing.T) { 10 | t.Parallel() 11 | assert := assert.New(t) 12 | var m map[string]int 13 | v, found := Pop(m, "d") 14 | assert.Zero(v) 15 | assert.False(found) 16 | 17 | m = map[string]int{ 18 | "a": 1, 19 | "b": 2, 20 | "c": 3, 21 | } 22 | v, found = Pop(m, "d") 23 | assert.Zero(v) 24 | assert.False(found) 25 | 26 | assert.Contains(m, "a") 27 | v, found = Pop(m, "a") 28 | assert.Equal(1, v) 29 | assert.True(found) 30 | 31 | assert.Equal( 32 | map[string]int{ 33 | "b": 2, 34 | "c": 3, 35 | }, 36 | m, 37 | ) 38 | } 39 | 40 | func TestSliceToSet(t *testing.T) { 41 | t.Parallel() 42 | assert := assert.New(t) 43 | var slice []string 44 | 45 | assert.Equal(map[string]struct{}{}, SliceToSet(slice)) 46 | 47 | slice = []string{"a", "b", "c", "c", "b"} 48 | 49 | assert.Equal(map[string]struct{}{ 50 | "a": {}, 51 | "b": {}, 52 | "c": {}, 53 | }, SliceToSet(slice)) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/utils/slices.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/blackstork-io/fabric/pkg/diagnostics" 4 | 5 | // Sets slice[idx] = val, growing the slice if needed, and returns the updated slice. 6 | func SetAt[T any](slice []T, idx int, val T) []T { 7 | needToAlloc := idx - len(slice) 8 | switch { 9 | case needToAlloc > 0: 10 | slice = append(slice, make([]T, needToAlloc)...) 11 | 12 | fallthrough 13 | case needToAlloc == 0: 14 | slice = append(slice, val) 15 | default: 16 | slice[idx] = val 17 | } 18 | return slice 19 | } 20 | 21 | // Produce a new slice by applying function fn to items of the slice s. 22 | func FnMap[I, O any](s []I, fn func(I) O) []O { 23 | if s == nil { 24 | return nil 25 | } 26 | out := make([]O, len(s)) 27 | for i, v := range s { 28 | out[i] = fn(v) 29 | } 30 | return out 31 | } 32 | 33 | // Produce a new slice by applying (possibly erroring) function fn to items of the slice s. 34 | // Returns on the first error with nil slice. 35 | func FnMapErr[I, O any](s []I, fn func(I) (O, error)) (out []O, err error) { 36 | if s == nil { 37 | return nil, nil 38 | } 39 | out = make([]O, len(s)) 40 | for i, v := range s { 41 | out[i], err = fn(v) 42 | if err != nil { 43 | out = nil 44 | break 45 | } 46 | } 47 | return 48 | } 49 | 50 | // Produce a new slice by applying function fn to items of the slice s. 51 | // Collects slice-like errors from the second return value (diagnostics in our case) 52 | func FnMapDiags[I, O any](diags *diagnostics.Diag, s []I, fn func(I) (O, diagnostics.Diag)) []O { 53 | if s == nil { 54 | return nil 55 | } 56 | var diag diagnostics.Diag 57 | out := make([]O, len(s)) 58 | for i, v := range s { 59 | out[i], diag = fn(v) 60 | diags.Extend(diag) 61 | } 62 | return out 63 | } 64 | -------------------------------------------------------------------------------- /pkg/utils/slices_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "slices" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSetAt(t *testing.T) { 12 | t.Parallel() 13 | 14 | type testCase struct { 15 | name string 16 | slice []int 17 | idx int 18 | val int 19 | } 20 | 21 | tests := []testCase{ 22 | { 23 | name: "Regular append", 24 | slice: []int{0, 1, 2}, 25 | idx: 3, 26 | val: 1337, 27 | }, 28 | { 29 | name: "Set existing", 30 | slice: []int{0, 1, 2}, 31 | idx: 1, 32 | val: 1337, 33 | }, 34 | { 35 | name: "Set and extend", 36 | slice: []int{0, 1, 2}, 37 | idx: 10, 38 | val: 1337, 39 | }, 40 | } 41 | 42 | for _, tc := range tests { 43 | tc := tc 44 | t.Run(tc.name, func(t *testing.T) { 45 | t.Parallel() 46 | assert := assert.New(t) 47 | orig := slices.Clone(tc.slice) 48 | res := SetAt(tc.slice, tc.idx, tc.val) 49 | 50 | assert.Len(res, max(len(orig), tc.idx+1)) 51 | 52 | for i := range res { 53 | switch { 54 | case i == tc.idx: // value at `idx` should become `val` 55 | assert.Equal(tc.val, res[i]) 56 | case i < len(orig): // other values from the original slice shouldn't change 57 | assert.Equal(orig[i], res[i]) 58 | default: // i >= len(orig) 59 | // other values should be filled by the zero value 60 | assert.Zero(res[i]) 61 | } 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestFnMap(t *testing.T) { 68 | t.Parallel() 69 | assert := assert.New(t) 70 | 71 | assert.Equal([]string{"1", "2", "3"}, FnMap([]int{1, 2, 3}, strconv.Itoa)) 72 | assert.Equal([]string{}, FnMap([]int{}, strconv.Itoa)) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/utils/string_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/blackstork-io/fabric/pkg/utils" 9 | ) 10 | 11 | func TestMemoizedKeys(t *testing.T) { 12 | t.Parallel() 13 | assert := assert.New(t) 14 | type testCase struct { 15 | name string 16 | m map[string]struct{} 17 | want string 18 | } 19 | tests := []testCase{ 20 | { 21 | name: "None", 22 | m: map[string]struct{}{}, 23 | want: "", 24 | }, 25 | { 26 | name: "One", 27 | m: map[string]struct{}{ 28 | "one": {}, 29 | }, 30 | want: "'one'", 31 | }, 32 | { 33 | name: "Two", 34 | m: map[string]struct{}{ 35 | "one": {}, 36 | "two": {}, 37 | }, 38 | want: "'one', 'two'", 39 | }, 40 | 41 | { 42 | name: "Sorted", 43 | m: map[string]struct{}{ 44 | "E": {}, 45 | "D": {}, 46 | "C": {}, 47 | "B": {}, 48 | "A": {}, 49 | }, 50 | want: "'A', 'B', 'C', 'D', 'E'", 51 | }, 52 | } 53 | for _, tc := range tests { 54 | tc := tc 55 | t.Run(tc.name, func(t *testing.T) { 56 | t.Parallel() 57 | assert.Equal(tc.want, utils.MemoizedKeys(&tc.m)()) 58 | }) 59 | } 60 | } 61 | 62 | func TestMemoizedKeysMemoizes(t *testing.T) { 63 | assert := assert.New(t) 64 | m := map[string]struct{}{ 65 | "A": {}, 66 | "B": {}, 67 | } 68 | mk := utils.MemoizedKeys(&m) 69 | m["C"] = struct{}{} 70 | mkRes := mk() 71 | assert.Equal("'A', 'B', 'C'", mkRes) 72 | m["D"] = struct{}{} 73 | mkRes2 := mk() 74 | assert.Equal("'A', 'B', 'C'", mkRes2) 75 | } 76 | -------------------------------------------------------------------------------- /plugin/ast/astsrc/astsrc.go: -------------------------------------------------------------------------------- 1 | package astsrc 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yuin/goldmark/text" 7 | ) 8 | 9 | // ASTSource holds the source of the markdown AST (a read-only byte slice). 10 | type ASTSource []byte 11 | 12 | // Append appends bytes to the source and returns corresponding segment. 13 | func (s *ASTSource) Append(data []byte) text.Segment { 14 | start := len(*s) 15 | *s = append(*s, data...) 16 | return text.NewSegment(start, len(*s)) 17 | } 18 | 19 | // AppendMultiple appends multiple byte slices to the source and returns corresponding segments. 20 | func (s *ASTSource) AppendMultiple(data [][]byte) *text.Segments { 21 | values := make([]text.Segment, len(data)) 22 | for i, segment := range data { 23 | values[i] = s.Append(segment) 24 | } 25 | res := text.NewSegments() 26 | res.AppendAll(values) 27 | return res 28 | } 29 | 30 | // AppendString appends a string to the source and returns corresponding segment. 31 | func (s *ASTSource) AppendString(data string) text.Segment { 32 | return s.Append([]byte(data)) 33 | } 34 | 35 | // Appendf appends formatted string to the source and returns corresponding segment. 36 | func (s *ASTSource) Appendf(format string, args ...interface{}) text.Segment { 37 | start := len(*s) 38 | *s = fmt.Appendf(*s, format, args...) 39 | return text.NewSegment(start, len(*s)) 40 | } 41 | 42 | // AsBytes returns the source as a byte slice. 43 | // Returned bytes should be treated as read-only or modified with care, 44 | // ensuring that the offsets are not changed. 45 | func (s ASTSource) AsBytes() []byte { 46 | return s 47 | } 48 | -------------------------------------------------------------------------------- /plugin/ast/nodes/content_node.go: -------------------------------------------------------------------------------- 1 | package nodes 2 | 3 | import "github.com/yuin/goldmark/ast" 4 | 5 | type FabricContentNode struct { 6 | ast.BaseBlock 7 | Meta *ContentMeta 8 | } 9 | 10 | func ToFabricContentNode(node ast.Node) (meta *FabricContentNode) { 11 | switch n := node.(type) { 12 | case *FabricContentNode: 13 | meta = n 14 | case *ast.Document: 15 | meta = &FabricContentNode{} 16 | child := n.FirstChild() 17 | for child != nil { 18 | c := child 19 | child = child.NextSibling() 20 | meta.AppendChild(meta, c) 21 | } 22 | case nil: 23 | // meta is nil 24 | default: 25 | meta = &FabricContentNode{} 26 | meta.AppendChild(meta, n) 27 | } 28 | return 29 | } 30 | 31 | // Dump implements ast.Node. 32 | func (m *FabricContentNode) Dump(source []byte, level int) { 33 | var kv map[string]string 34 | if m == nil || m.Meta == nil { 35 | kv = map[string]string{ 36 | "meta": "nil", 37 | } 38 | } else { 39 | kv = map[string]string{ 40 | "meta.provider": m.Meta.Provider, 41 | "meta.plugin": m.Meta.Plugin, 42 | "meta.version": m.Meta.Version, 43 | } 44 | } 45 | ast.DumpHelper(m, source, level, kv, nil) 46 | } 47 | 48 | // Kind implements ast.Node. 49 | func (m *FabricContentNode) Kind() ast.NodeKind { 50 | return ContentNodeKind 51 | } 52 | 53 | var _ ast.Node = &FabricContentNode{} 54 | -------------------------------------------------------------------------------- /plugin/ast/nodes/metadata.go: -------------------------------------------------------------------------------- 1 | package nodes 2 | 3 | import "github.com/blackstork-io/fabric/plugin/plugindata" 4 | 5 | type ContentMeta struct { 6 | Provider string 7 | Plugin string 8 | Version string 9 | } 10 | 11 | func (meta *ContentMeta) AsData() plugindata.Data { 12 | if meta == nil { 13 | return nil 14 | } 15 | return plugindata.Map{ 16 | "provider": plugindata.String(meta.Provider), 17 | "plugin": plugindata.String(meta.Plugin), 18 | "version": plugindata.String(meta.Version), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin/ast/v1/bubble_error.go: -------------------------------------------------------------------------------- 1 | package astv1 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // bubbleError is a wrapper for errors that are meant to be panic'ed 8 | // and caught by the higher level caller. 9 | // This is considered to be a bad practice, but return-error propagation 10 | // is not practical in deeply nested encoding/decoding functions. 11 | // Standard library uses this pattern in some places, i.e. encoding/json. 12 | type bubbleError struct { 13 | err error 14 | } 15 | 16 | // bubbleWrap wraps an error in a bubbleError and returns it (not panics). 17 | func bubbleWrap(err error) bubbleError { 18 | return bubbleError{err: err} 19 | } 20 | 21 | // bubbleUp wraps an error in a bubbleError and panics it. 22 | func bubbleUp(err error) { 23 | panic(bubbleWrap(err)) 24 | } 25 | 26 | // recoverBubbleError expects to be called in a defer statement with 27 | // the current error and a recover(). If bubbleError is recovered, it 28 | // would be [errors.Join]'ed with the current error (if non nil) and 29 | // returned. 30 | // If there was no panic, the passed in error would be returned. 31 | // If there is any other panic, it would be re-panicked. 32 | func recoverBubbleError(err error, recovered any) error { 33 | if recovered == nil { 34 | return err 35 | } 36 | if pErr, ok := recovered.(bubbleError); ok { 37 | if err != nil { 38 | return errors.Join(err, pErr.err) 39 | } 40 | return pErr.err 41 | } 42 | panic(recovered) 43 | } 44 | -------------------------------------------------------------------------------- /plugin/ast/v1/testdata/fuzz/FuzzEncoder/6b5e47038908d6af: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte(" ```\n\t") 3 | -------------------------------------------------------------------------------- /plugin/dataspec/attr_spec_doc_comment.gotmpl: -------------------------------------------------------------------------------- 1 | {{- if .Doc -}} 2 | {{ .Doc }} 3 | 4 | {{ end -}} 5 | {{ if .IsRequired }}Required{{ else }}Optional{{ end }} {{ .Type }}. 6 | {{- if .OneOf }} 7 | Must be one of: {{ .OneOf }} 8 | {{- end }} 9 | {{- if and .MinVal .MaxVal }} 10 | Must be between {{ .MinVal }} and {{ .MaxVal }} (inclusive) 11 | {{- else if .MinVal }} 12 | Must be >= {{ .MinVal }} 13 | {{- else if .MaxVal }} 14 | Must be <= {{ .MaxVal }} 15 | {{- end }} 16 | {{- if and .MinLenVal .MaxLenVal -}} 17 | {{- if eq .MinLenVal .MaxLenVal }} 18 | Must have a length of {{ .MinLenVal }} 19 | {{- else }} 20 | Must have a length between {{ .MinLenVal }} and {{ .MaxLenVal }} (inclusive) 21 | {{- end -}} 22 | {{- else if .MinLenVal -}} 23 | {{- if eq .MinLenVal "1" }} 24 | Must be non-empty 25 | {{- else }} 26 | Must contain at least {{ .MinLenVal }} elements. 27 | {{- end -}} 28 | {{- else if .MaxLenVal }} 29 | Must contain no more than {{ .MaxLenVal }} elements. 30 | {{- end -}} 31 | {{- if .IsRequired }} 32 | 33 | For example: 34 | {{- else -}} 35 | {{- if .Example }} 36 | 37 | For example: 38 | {{ .Example }} 39 | {{- end }} 40 | Default value: 41 | {{- end -}} 42 | -------------------------------------------------------------------------------- /plugin/dataspec/constraint/constraint_test.go: -------------------------------------------------------------------------------- 1 | package constraint 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // lock constraints to their expected values 10 | // changing these would be backwards incompatible (for protobuf) 11 | func TestConstraints(t *testing.T) { 12 | assert := assert.New(t) 13 | assert.EqualValues(1, Required) 14 | assert.EqualValues(2, NonNull) 15 | assert.EqualValues(4, NonEmpty) 16 | assert.EqualValues(8, TrimSpace) 17 | assert.EqualValues(16, Integer) 18 | } 19 | -------------------------------------------------------------------------------- /plugin/dataspec/dataspec.go: -------------------------------------------------------------------------------- 1 | // Documentable wrapper types form 2 | package dataspec 3 | 4 | import ( 5 | "bytes" 6 | "strings" 7 | 8 | "github.com/hashicorp/hcl/v2/hclwrite" 9 | ) 10 | 11 | type ( 12 | Blocks []*Block 13 | Attributes map[string]*Attr 14 | ) 15 | 16 | // RenderDoc renders the block documentation for spec. 17 | func RenderDoc(spec *RootSpec, blockName string, labels ...string) string { 18 | // Special-casing the first line generation: 19 | // config "data" "csv" { -> config data csv { 20 | 21 | if strings.Contains(blockName, " ") { 22 | return "" 23 | } 24 | 25 | f := hclwrite.NewEmptyFile() 26 | spec.BlockSpec().WriteBodyDoc(f.Body().AppendNewBlock(blockName, labels).Body()) 27 | doc := hclwrite.Format(f.Bytes()) 28 | blockBodyStart := bytes.IndexByte(doc, '{') 29 | if blockBodyStart == -1 { 30 | return "" 31 | } 32 | 33 | header := formatHeader(blockName, labels) 34 | newStart := blockBodyStart - len(header) 35 | if newStart >= 0 { 36 | copy(doc[newStart:], header) 37 | doc = doc[newStart:] 38 | } else { 39 | doc = append(header, doc[blockBodyStart:]...) 40 | } 41 | 42 | return string(doc) 43 | } 44 | -------------------------------------------------------------------------------- /plugin/errors.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "fmt" 4 | 5 | var ErrContentLocationNotFound = fmt.Errorf("content location not found") 6 | -------------------------------------------------------------------------------- /plugin/pluginapi/v1/data_decoder.go: -------------------------------------------------------------------------------- 1 | package pluginapiv1 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/blackstork-io/fabric/pkg/utils" 7 | "github.com/blackstork-io/fabric/plugin/plugindata" 8 | ) 9 | 10 | func decodeData(src *Data) plugindata.Data { 11 | switch src.GetData().(type) { 12 | case nil: 13 | return nil 14 | case *Data_NumberVal: 15 | return plugindata.Number(src.GetNumberVal()) 16 | case *Data_StringVal: 17 | return plugindata.String(src.GetStringVal()) 18 | case *Data_BoolVal: 19 | return plugindata.Bool(src.GetBoolVal()) 20 | case *Data_MapVal: 21 | return decodeMapData(src.GetMapVal().GetValue()) 22 | case *Data_ListVal: 23 | return plugindata.List(utils.FnMap(src.GetListVal().GetValue(), decodeData)) 24 | case *Data_TimeVal: 25 | return plugindata.Time(src.GetTimeVal().AsTime()) 26 | } 27 | panic(fmt.Sprintf("Unexpected src data type: %T", src.GetData())) 28 | } 29 | 30 | func decodeMapData(src map[string]*Data) plugindata.Map { 31 | return plugindata.Map(utils.MapMap(src, decodeData)) 32 | } 33 | -------------------------------------------------------------------------------- /plugin/pluginapi/v1/data_encoder.go: -------------------------------------------------------------------------------- 1 | package pluginapiv1 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "google.golang.org/protobuf/types/known/timestamppb" 8 | 9 | "github.com/blackstork-io/fabric/pkg/utils" 10 | "github.com/blackstork-io/fabric/plugin/plugindata" 11 | ) 12 | 13 | func encodeData(d plugindata.Data) *Data { 14 | switch v := d.(type) { 15 | case nil: 16 | return nil 17 | case plugindata.Number: 18 | return &Data{ 19 | Data: &Data_NumberVal{ 20 | NumberVal: float64(v), 21 | }, 22 | } 23 | case plugindata.String: 24 | return &Data{ 25 | Data: &Data_StringVal{ 26 | StringVal: string(v), 27 | }, 28 | } 29 | case plugindata.Bool: 30 | return &Data{ 31 | Data: &Data_BoolVal{ 32 | BoolVal: bool(v), 33 | }, 34 | } 35 | case plugindata.Map: 36 | return &Data{ 37 | Data: &Data_MapVal{ 38 | encodeMapData(v), 39 | }, 40 | } 41 | case plugindata.List: 42 | return &Data{ 43 | Data: &Data_ListVal{ 44 | ListVal: &ListData{ 45 | Value: utils.FnMap(v, encodeData), 46 | }, 47 | }, 48 | } 49 | case plugindata.Time: 50 | return &Data{ 51 | Data: &Data_TimeVal{ 52 | TimeVal: timestamppb.New(time.Time(d.(plugindata.Time))), 53 | }, 54 | } 55 | default: 56 | if cd, ok := d.(plugindata.Convertible); ok { 57 | return encodeData(cd.AsPluginData()) 58 | } 59 | } 60 | panic(fmt.Errorf("unexpected plugin data type: %T", d)) 61 | } 62 | 63 | func encodeMapData(m plugindata.Map) *MapData { 64 | return &MapData{ 65 | Value: utils.MapMap(m, encodeData), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /plugin/pluginapi/v1/hcl_decoder.go: -------------------------------------------------------------------------------- 1 | package pluginapiv1 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | 6 | "github.com/blackstork-io/fabric/pkg/utils" 7 | ) 8 | 9 | func decodeDiagnosticList(src []*Diagnostic) []*hcl.Diagnostic { 10 | return utils.FnMap(src, decodeDiagnostic) 11 | } 12 | 13 | func decodeDiagnostic(src *Diagnostic) *hcl.Diagnostic { 14 | if src == nil { 15 | return nil 16 | } 17 | return &hcl.Diagnostic{ 18 | Severity: hcl.DiagnosticSeverity(src.GetSeverity()), 19 | Summary: src.GetSummary(), 20 | Detail: src.GetDetail(), 21 | Subject: decodeRange(src.GetSubject()).Ptr(), 22 | Context: decodeRange(src.GetContext()).Ptr(), 23 | } 24 | } 25 | 26 | func decodePos(src *Pos) hcl.Pos { 27 | if src == nil { 28 | return hcl.InitialPos 29 | } 30 | return hcl.Pos{ 31 | Line: int(src.GetLine()), 32 | Column: int(src.GetColumn()), 33 | Byte: int(src.GetByte()), 34 | } 35 | } 36 | 37 | func decodeRange(src *Range) hcl.Range { 38 | if src == nil { 39 | return hcl.Range{ 40 | Filename: "", 41 | Start: hcl.InitialPos, 42 | End: hcl.InitialPos, 43 | } 44 | } 45 | return hcl.Range{ 46 | Filename: src.GetFilename(), 47 | Start: decodePos(src.GetStart()), 48 | End: decodePos(src.GetEnd()), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /plugin/pluginapi/v1/hcl_encoder.go: -------------------------------------------------------------------------------- 1 | package pluginapiv1 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | 6 | "github.com/blackstork-io/fabric/pkg/utils" 7 | ) 8 | 9 | func encodeDiagnosticList(src []*hcl.Diagnostic) []*Diagnostic { 10 | return utils.FnMap(src, encodeDiagnostic) 11 | } 12 | 13 | func encodeDiagnostic(src *hcl.Diagnostic) *Diagnostic { 14 | if src == nil { 15 | return nil 16 | } 17 | return &Diagnostic{ 18 | Severity: int64(src.Severity), 19 | Summary: src.Summary, 20 | Detail: src.Detail, 21 | Subject: encodeRange(src.Subject), 22 | Context: encodeRange(src.Context), 23 | } 24 | } 25 | 26 | func encodePos(src hcl.Pos) *Pos { 27 | return &Pos{ 28 | Line: int64(src.Line), 29 | Column: int64(src.Column), 30 | Byte: int64(src.Byte), 31 | } 32 | } 33 | 34 | func encodeRange(src *hcl.Range) *Range { 35 | if src == nil { 36 | return nil 37 | } 38 | return &Range{ 39 | Filename: src.Filename, 40 | Start: encodePos(src.Start), 41 | End: encodePos(src.End), 42 | } 43 | } 44 | 45 | func encodeRangeVal(src hcl.Range) *Range { 46 | return encodeRange(&src) 47 | } 48 | -------------------------------------------------------------------------------- /plugin/pluginapi/v1/is_not_plugin.go: -------------------------------------------------------------------------------- 1 | //go:build !fabricplugin 2 | 3 | package pluginapiv1 4 | 5 | import ( 6 | "github.com/hashicorp/go-hclog" 7 | ) 8 | 9 | func loggerForGoplugin() hclog.Logger { 10 | panic("Attempted to run a plugin built without `fabricplugin` tag") 11 | } 12 | -------------------------------------------------------------------------------- /plugin/resolver/options.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "io" 5 | "log/slog" 6 | 7 | "go.opentelemetry.io/otel/trace" 8 | tracenoop "go.opentelemetry.io/otel/trace/noop" 9 | ) 10 | 11 | // options for the resolver. 12 | type options struct { 13 | logger *slog.Logger 14 | tracer trace.Tracer 15 | sources []Source 16 | } 17 | 18 | var defaultOptions = options{ 19 | logger: slog.New(slog.NewTextHandler(io.Discard, nil)), 20 | sources: []Source{}, 21 | tracer: tracenoop.Tracer{}, 22 | } 23 | 24 | // Option is a functional option for the resolver. 25 | type Option func(*options) 26 | 27 | // WithLogger sets the logger for the resolver. 28 | func WithLogger(logger *slog.Logger) Option { 29 | return func(o *options) { 30 | o.logger = logger 31 | } 32 | } 33 | 34 | // WithSources sets the sources for the resolver. 35 | func WithSources(sources ...Source) Option { 36 | return func(o *options) { 37 | o.sources = sources 38 | } 39 | } 40 | 41 | // WithTracer sets the tracer for the resolver. 42 | func WithTracer(tracer trace.Tracer) Option { 43 | return func(o *options) { 44 | o.tracer = tracer 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /plugin/resolver/version.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/Masterminds/semver/v3" 8 | ) 9 | 10 | // Version is a version of a plugin. It is a wrapper around semver.Version with strict parsing. 11 | type Version struct { 12 | *semver.Version 13 | } 14 | 15 | // UnmarshalJSON parses a JSON string into a PluginVersion using strict semver parsing. 16 | func (v *Version) UnmarshalJSON(data []byte) error { 17 | raw, err := strconv.Unquote(string(data)) 18 | if err != nil { 19 | return fmt.Errorf("failed to unquote version: %w", err) 20 | } 21 | ver, err := semver.StrictNewVersion(raw) 22 | if err != nil { 23 | return err 24 | } 25 | *v = Version{ver} 26 | return nil 27 | } 28 | 29 | // Compare compares the version with another version. 30 | func (v Version) Compare(other Version) int { 31 | return v.Version.Compare(other.Version) 32 | } 33 | 34 | // ConstraintMap is a map of plugin names to version constraints. 35 | type ConstraintMap map[Name]*semver.Constraints 36 | 37 | // ParseConstraintMap parses string map into a PluginConstraintMap. 38 | func ParseConstraintMap(src map[string]string) (ConstraintMap, error) { 39 | if src == nil { 40 | return nil, nil 41 | } 42 | parsed := make(ConstraintMap) 43 | for name, version := range src { 44 | if version == "" { 45 | return nil, fmt.Errorf("missing plugin version constraint for '%s'", name) 46 | } 47 | parsedName, err := ParseName(name) 48 | if err != nil { 49 | return nil, err 50 | } 51 | constraints, err := semver.NewConstraint(version) 52 | if err != nil { 53 | return nil, err 54 | } 55 | parsed[parsedName] = constraints 56 | } 57 | return parsed, nil 58 | } 59 | -------------------------------------------------------------------------------- /print/htmlprint/document.gotempl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{- if .Description}} 7 | 8 | {{- end}} 9 | {{.Title}} 10 | {{- range .JSSources}} 11 | 12 | {{- end}} 13 | {{- range .CSSSources}} 14 | 15 | {{- end}} 16 | {{- if .JS}} 17 | 20 | {{- end}} 21 | {{- if .CSS}} 22 | 25 | {{- end}} 26 | 27 | 28 | {{.Content}} 29 | 30 | -------------------------------------------------------------------------------- /print/logging.go: -------------------------------------------------------------------------------- 1 | package print 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log/slog" 7 | 8 | "github.com/blackstork-io/fabric/plugin" 9 | ) 10 | 11 | type logging struct { 12 | next Printer 13 | logger *slog.Logger 14 | attrs []slog.Attr 15 | } 16 | 17 | // WithLogging wraps the printer with logging instrumentation. 18 | func WithLogging(next Printer, logger *slog.Logger, attrs ...slog.Attr) Printer { 19 | return &logging{ 20 | next: next, 21 | logger: logger, 22 | attrs: attrs, 23 | } 24 | } 25 | 26 | func (p logging) Print(ctx context.Context, w io.Writer, el plugin.Content) (err error) { 27 | p.logger.LogAttrs(ctx, slog.LevelDebug, "Printing content", p.attrs...) 28 | return p.next.Print(ctx, w, el) 29 | } 30 | -------------------------------------------------------------------------------- /print/tracing.go: -------------------------------------------------------------------------------- 1 | package print 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/codes" 9 | "go.opentelemetry.io/otel/trace" 10 | 11 | "github.com/blackstork-io/fabric/plugin" 12 | ) 13 | 14 | type tracing struct { 15 | next Printer 16 | tracer trace.Tracer 17 | attrs []attribute.KeyValue 18 | } 19 | 20 | // WithTracing wraps a printer with tracing instrumentation. 21 | func WithTracing(next Printer, tracer trace.Tracer, attrs ...attribute.KeyValue) Printer { 22 | return tracing{ 23 | next: next, 24 | tracer: tracer, 25 | attrs: attrs, 26 | } 27 | } 28 | 29 | func (p tracing) Print(ctx context.Context, w io.Writer, el plugin.Content) (err error) { 30 | ctx, span := p.tracer.Start(ctx, "Printer.Print", trace.WithAttributes(p.attrs...)) 31 | defer func() { 32 | if err != nil { 33 | span.RecordError(err) 34 | span.SetStatus(codes.Error, err.Error()) 35 | } else { 36 | span.SetStatus(codes.Ok, "success") 37 | } 38 | span.End() 39 | }() 40 | return p.next.Print(ctx, w, el) 41 | } 42 | -------------------------------------------------------------------------------- /proto/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | breaking: 3 | use: 4 | - FILE 5 | lint: 6 | use: 7 | - DEFAULT 8 | -------------------------------------------------------------------------------- /proto/pluginapi/v1/content.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pluginapi.v1; 4 | import "ast/v1/ast.proto"; 5 | 6 | 7 | enum LocationEffect { 8 | LOCATION_EFFECT_UNSPECIFIED = 0; 9 | LOCATION_EFFECT_BEFORE = 1; 10 | LOCATION_EFFECT_AFTER = 2; 11 | } 12 | 13 | message Location { 14 | uint32 index = 1; 15 | LocationEffect effect = 2; 16 | } 17 | 18 | message ContentResult { 19 | Content content = 1; 20 | Location location = 2; 21 | } 22 | 23 | message Content { 24 | oneof value { 25 | ContentElement element = 1; 26 | ContentSection section = 2; 27 | ContentEmpty empty = 3; 28 | }; 29 | } 30 | 31 | message ContentSection { 32 | repeated Content children = 1; 33 | ast.v1.Metadata meta = 2; 34 | } 35 | 36 | message ContentElement { 37 | bytes markdown = 1; 38 | optional ast.v1.FabricContentNode ast = 2; 39 | ast.v1.Metadata meta = 3; 40 | } 41 | 42 | message ContentEmpty {} -------------------------------------------------------------------------------- /proto/pluginapi/v1/data.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/timestamp.proto"; 4 | 5 | package pluginapi.v1; 6 | 7 | message Data { 8 | oneof data { 9 | string string_val = 1; 10 | double number_val = 2; 11 | bool bool_val = 3; 12 | ListData list_val = 4; 13 | MapData map_val = 5; 14 | google.protobuf.Timestamp time_val = 6; 15 | }; 16 | } 17 | 18 | message ListData { 19 | repeated Data value = 1; 20 | } 21 | 22 | message MapData { 23 | map value = 1; 24 | } 25 | -------------------------------------------------------------------------------- /proto/pluginapi/v1/hcl.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pluginapi.v1; 4 | 5 | message Pos { 6 | int64 line = 1; 7 | int64 column = 2; 8 | int64 byte = 3; 9 | } 10 | 11 | message Range { 12 | string filename = 1; 13 | Pos start = 2; 14 | Pos end = 3; 15 | } 16 | 17 | message Diagnostic { 18 | int64 severity = 1; 19 | string summary = 2; 20 | string detail = 3; 21 | Range subject = 4; 22 | Range context = 5; 23 | } 24 | -------------------------------------------------------------------------------- /proto/pluginapi/v1/plugin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pluginapi.v1; 4 | 5 | import "pluginapi/v1/content.proto"; 6 | import "pluginapi/v1/data.proto"; 7 | import "pluginapi/v1/hcl.proto"; 8 | import "pluginapi/v1/schema.proto"; 9 | import "pluginapi/v1/dataspec.proto"; 10 | 11 | 12 | service PluginService { 13 | rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse) {} 14 | rpc RetrieveData(RetrieveDataRequest) returns (RetrieveDataResponse) {} 15 | rpc ProvideContent(ProvideContentRequest) returns (ProvideContentResponse) {} 16 | rpc Publish(PublishRequest) returns (PublishResponse) {} 17 | } 18 | 19 | message GetSchemaRequest {} 20 | 21 | message GetSchemaResponse { 22 | Schema schema = 1; 23 | } 24 | 25 | message RetrieveDataRequest { 26 | string source = 1; 27 | Block args = 2; 28 | Block config = 3; 29 | } 30 | 31 | message RetrieveDataResponse { 32 | Data data = 1; 33 | repeated Diagnostic diagnostics = 2; 34 | } 35 | 36 | message ProvideContentRequest { 37 | string provider = 1; 38 | Block args = 2; 39 | Block config = 3; 40 | MapData data_context = 4; 41 | uint32 content_id = 5; 42 | } 43 | 44 | message ProvideContentResponse { 45 | ContentResult result = 1; 46 | repeated Diagnostic diagnostics = 2; 47 | } 48 | 49 | 50 | message PublishRequest { 51 | string publisher = 1; 52 | Block args = 2; 53 | Block config = 3; 54 | MapData data_context = 4; 55 | OutputFormat format = 5; 56 | string document_name = 6; 57 | } 58 | 59 | message PublishResponse { 60 | repeated Diagnostic diagnostics = 1; 61 | } -------------------------------------------------------------------------------- /proto/pluginapi/v1/schema.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pluginapi.v1; 4 | 5 | import "pluginapi/v1/dataspec.proto"; 6 | 7 | message Schema { 8 | string name = 1; 9 | string version = 2; 10 | // Plugin components 11 | map data_sources = 3; 12 | map content_providers = 4; 13 | map publishers = 7; 14 | string doc = 5; 15 | repeated string tags = 6; 16 | } 17 | 18 | message DataSourceSchema { 19 | BlockSpec args = 3; 20 | BlockSpec config = 4; 21 | string doc = 5; 22 | repeated string tags = 6; 23 | } 24 | 25 | enum InvocationOrder { 26 | INVOCATION_ORDER_UNSPECIFIED = 0; 27 | INVOCATION_ORDER_BEGIN = 2; 28 | INVOCATION_ORDER_END = 3; 29 | } 30 | 31 | message ContentProviderSchema { 32 | BlockSpec args = 4; 33 | BlockSpec config = 5; 34 | InvocationOrder invocation_order = 3; 35 | string doc = 6; 36 | repeated string tags = 7; 37 | } 38 | 39 | 40 | enum OutputFormat { 41 | OUTPUT_FORMAT_UNSPECIFIED = 0; 42 | OUTPUT_FORMAT_MD = 1; 43 | OUTPUT_FORMAT_HTML = 2; 44 | OUTPUT_FORMAT_PDF = 3; 45 | } 46 | 47 | message PublisherSchema { 48 | BlockSpec args = 1; 49 | BlockSpec config = 2; 50 | string doc = 3; 51 | repeated string tags = 4; 52 | repeated OutputFormat allowed_formats = 5; 53 | } -------------------------------------------------------------------------------- /tools/pluginmeta/metadata.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Metadata struct { 4 | Plugins []*PluginMetadata `json:"plugins"` 5 | } 6 | 7 | type PluginMetadata struct { 8 | Name string `json:"name"` 9 | Version string `json:"version"` 10 | Archives []*PluginArchiveMetadata `json:"archives"` 11 | } 12 | 13 | type PluginArchiveMetadata struct { 14 | Filename string `json:"filename"` 15 | OS string `json:"os"` 16 | Arch string `json:"arch"` 17 | BinaryChecksum string `json:"binary_checksum"` 18 | } 19 | -------------------------------------------------------------------------------- /tools/pluginmeta/releaser_config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type ReleaserConfig struct { 4 | Builds []ReleaserBuild `yaml:"builds"` 5 | Archives []ReleaserArchive `yaml:"archives"` 6 | } 7 | 8 | type ReleaserBuild struct { 9 | ID string `yaml:"id"` 10 | GOOS []string `yaml:"goos"` 11 | } 12 | 13 | type ReleaserArchive struct { 14 | ID string `yaml:"id"` 15 | Format string `yaml:"format"` 16 | Builds []string `yaml:"builds"` 17 | NameTemplate string `yaml:"name_template"` 18 | } 19 | 20 | type ReleaserFormatOverride struct { 21 | GOOS string `yaml:"goos"` 22 | Format string `yaml:"format"` 23 | } 24 | --------------------------------------------------------------------------------