jekyll-client-search 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d3574c3adc2e2e00dd76ef5b5011358fcb9abfe3ed0cfbd372dffee00f913fc
4
- data.tar.gz: 76f691ec62695d6e99677bc952fe85ae07c477df563f83b29f063a58f5b0d8c0
3
+ metadata.gz: 60acab6161a3d4eafb265742c337c86bbc26e444bc0ce202a02e5ce80049c3f0
4
+ data.tar.gz: bf05aa7d4945bb3e82acd8ab5127ab2e84bb457eb9868f79d235a8a5ce701a53
5
5
  SHA512:
6
- metadata.gz: 23b5534d2c60626e5ee068687f2c4313f6e7494f1e382b89928f37b3eaf50fad3f979808e696c6a2bf3d17a5a00764674667b9f8f4041b4fc6f73a71953fcee7
7
- data.tar.gz: 9403f90fd84ee7798fa07281001faf534028247e2f99aea46a9a305b8c3ac57191ae0059cb49ca8eb377b1ba7633b5960f3c481bc24e0cc6de872d30132b3521
6
+ metadata.gz: cc8c5846be7c3a812a94c59aea41ecbb58c9672fa5948b4a306193e6d5914fc18f7104263f9a0ec84a1d405099dbc72dfc6c38c84cc3135133111b3f1c199ab8
7
+ data.tar.gz: e5eb813ed9b6dc1ba7abf34a96bbefa1c353453d8596d79dbf0f04290c7b502f18306419f4b5adc33712a8a18896def5fe0389293ffe43f7f7fbed2622083d6c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.3 — 2026-09-11
6
+
7
+ ### Refactored
8
+ - Extract duplicated `normalize` function from `client-search-base.js` and `client-search-dropdown.js` into new shared module `assets/client-search-shared.js` (exposes `window.ClientSearchShared.normalize` and `window.ClientSearchShared.coreFields`) — eliminates CodeFactor duplicate-code finding and ensures consistent normalization across both runtimes
9
+
10
+ ## 0.3.2 — 2026-09-10
11
+
12
+ ### Added
13
+ - Semgrep security scan in CI (`semgrep ci` job in `.github/workflows/ci.yml`) — runs on every push and pull request using the `SEMGREP_APP_TOKEN` secret and the Semgrep Pro engine with the organization's full policy (2,900+ rules)
14
+ - `.semgrep.yml` with custom Ruby ReDoS detection rules and usage documentation
15
+ - Semgrep step in pre-commit hook (`bin/install-hooks.sh`)
16
+ - Dependabot configuration for GitHub Actions (`.github/dependabot.yml`) — weekly SHA-pinned action updates
17
+ - CodeFactor and Semgrep badges in README.md
18
+ - Documentation of CodeQL, Semgrep, CodeFactor, and Dependabot in README.md, README.developer.md, and AGENTS.md
19
+ - Semgrep usage guide in README.developer.md (local commands, nosemgrep annotations, SHA pinning)
20
+
21
+ ### Fixed
22
+ - Pin all GitHub Actions to full 40-character commit SHAs with version comments to prevent supply-chain attacks via mutable tag repointing (Semgrep: `github-actions-mutable-action-tag`)
23
+ - Replace `Object.assign({}, entry, ...)` in benchmark with explicit field construction to avoid mass-assignment taint (Semgrep: `insecure-object-assign`)
24
+ - Replace manual arXiv query string with `URI.encode_www_form` for correct URL encoding (Semgrep: `avoid-tainted-http-request`)
25
+
26
+ ### Changed
27
+ - Add `nosemgrep` annotations with justifications for false positives (static ERB template, array-form `Open3.capture2`, fixed-host arXiv request) and acceptable risks (local loopback Ollama HTTP in tests and benchmarks)
28
+
5
29
  ## 0.3.1 — 2026-09-09
6
30
 
7
31
  ### Fixed
data/README.developer.md CHANGED
@@ -24,9 +24,50 @@ are required for the default test run.
24
24
  ## Quality checks and git hooks
25
25
 
26
26
  The project uses a focused quality stack: RuboCop (style), bundler-audit
27
- (security), RSpec (Ruby tests), and `npm test` (JavaScript tests). This
28
- covers the actionable ground without the maintenance burden of additional
29
- code-smell tools on a small focused gem.
27
+ (security), RSpec (Ruby tests), `npm test` (JavaScript tests), and Semgrep
28
+ (security scanning). This covers the actionable ground without the
29
+ maintenance burden of additional code-smell tools on a small focused gem.
30
+
31
+ External automated code review services:
32
+ - [CodeQL](https://codeql.github.com) — GitHub native security scanning
33
+ (Ruby + JavaScript), runs on every push.
34
+ - [Semgrep](https://semgrep.dev) — CI security scan using the Semgrep Pro
35
+ engine with the organization's full policy (2,900+ rules). Runs as a CI
36
+ job (`semgrep ci`) and in the pre-commit hook (custom rules only). See
37
+ `.semgrep.yml` for local configuration and usage instructions.
38
+ - [CodeFactor](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search) —
39
+ automated code review (RuboCop, Brakeman, bundler-audit, duplication).
40
+ - [Dependabot](https://docs.github.com/en/code-security/dependabot) — weekly
41
+ GitHub Actions version updates (SHA-pinned actions updated automatically).
42
+
43
+ ### Semgrep usage
44
+
45
+ The project uses two Semgrep scanning modes:
46
+
47
+ 1. **Pre-commit hook** — runs `semgrep scan --config .semgrep.yml` with only
48
+ the custom ReDoS rules defined in `.semgrep.yml`. Fast (~3s).
49
+ 2. **CI job** — runs `semgrep ci` with the full organization policy from the
50
+ Semgrep AppSec Platform, including Pro engine rules. Findings are uploaded
51
+ to the dashboard for triage.
52
+
53
+ Local commands:
54
+
55
+ ```bash
56
+ semgrep scan --config .semgrep.yml --error lib/ assets/ # custom rules only
57
+ semgrep ci --dry-run # full org policy, no upload
58
+ semgrep ci # full org policy, upload to dashboard
59
+ ```
60
+
61
+ To suppress a false positive or acceptable risk, add an inline annotation
62
+ with the full rule ID and a justification:
63
+
64
+ ```ruby
65
+ # nosemgrep: problem-based-packs.insecure-transport.ruby-stdlib.net-http-request.net-http-request
66
+ # Acceptable risk: local loopback Ollama test endpoint.
67
+ ```
68
+
69
+ All GitHub Actions are pinned to full 40-character commit SHAs with version
70
+ comments. Dependabot updates the SHAs and version comments automatically.
30
71
 
31
72
  ### Rake tasks
32
73
 
@@ -55,12 +96,12 @@ This installs two hooks:
55
96
 
56
97
  | Hook | What it runs | When |
57
98
  | --- | --- | --- |
58
- | `pre-commit` | `rubocop` only (~2s) | Before each commit |
99
+ | `pre-commit` | `rubocop + semgrep` (~5s) | Before each commit |
59
100
  | `pre-push` | `rubocop + rspec` (~15s) | Before each push |
60
101
 
61
- The pre-commit hook is intentionally fast (style only) to avoid bypassing
62
- with `--no-verify`. The full test suite runs on pre-push and in CI. Skip
63
- either with `git commit --no-verify` or `git push --no-verify`.
102
+ The pre-commit hook is intentionally fast (style + security) to avoid
103
+ bypassing with `--no-verify`. The full test suite runs on pre-push and in
104
+ CI. Skip either with `git commit --no-verify` or `git push --no-verify`.
64
105
 
65
106
  ### CI checks
66
107
 
@@ -71,6 +112,7 @@ request across Ruby 3.2/3.3/3.4 and Node 22/24:
71
112
  - `bundle exec bundle-audit check --update` (Ruby dependency security)
72
113
  - `npm audit --audit-level=high` (JavaScript dependency security)
73
114
  - `npm outdated` (non-blocking — warns about outdated npm packages)
115
+ - `semgrep ci` (security scan with custom rules from `.semgrep.yml`)
74
116
 
75
117
  ## Build-time related articles
76
118
 
data/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
  [![License: AGPL v3](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue.svg)](LICENSE)
9
9
  [![DeepWiki](https://img.shields.io/badge/DeepWiki-docs-7B68EE.svg)](https://deepwiki.com/gundestrup/jekyll-client-search)
10
10
  [![Coverage](https://img.shields.io/badge/coverage-100%25%20branches-brightgreen.svg)](#testing-strategy)
11
+ [![Semgrep](https://img.shields.io/badge/semgrep-scanning-brightgreen.svg)](https://semgrep.dev)
12
+ [![CodeFactor](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search/badge)](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search)
11
13
 
12
14
  A Jekyll plugin that generates a JSON document index for client-side search.
13
15
  Supports lexical search ([MiniSearch](https://lucaong.github.io/minisearch/),
@@ -1140,10 +1142,30 @@ bundle exec rake ci
1140
1142
 
1141
1143
  GitHub Actions are configured in `.github/workflows/`:
1142
1144
 
1143
- - `ci.yml` runs the test and gem build checks on pushes and pull requests.
1145
+ - `ci.yml` runs the test and gem build checks on pushes and pull requests,
1146
+ plus a Semgrep security scan job.
1144
1147
  - `release.yml` runs on `v*` tags or manual dispatch, verifies the gem, and
1145
1148
  publishes it using RubyGems trusted publishing.
1146
1149
 
1150
+ Additional automated code quality and security services:
1151
+
1152
+ - [CodeQL](https://codeql.github.com) — GitHub native code scanning for
1153
+ security vulnerabilities (Ruby + JavaScript).
1154
+ - [Semgrep](https://semgrep.dev) — CI security scan using the Semgrep Pro
1155
+ engine with the organization's full policy (2,900+ rules). Custom ReDoS
1156
+ detection rules are defined in `.semgrep.yml`. Findings are triaged on the
1157
+ [Semgrep AppSec Platform](https://semgrep.dev) dashboard. False positives
1158
+ and acceptable risks are annotated inline with `# nosemgrep: rule.id`.
1159
+ - [CodeFactor](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search) —
1160
+ automated code review with RuboCop, Brakeman, bundler-audit, and duplication
1161
+ checks.
1162
+ - [Dependabot](https://docs.github.com/en/code-security/dependabot) — weekly
1163
+ checks for GitHub Actions version updates (SHA-pinned actions are kept
1164
+ up to date automatically).
1165
+
1166
+ All GitHub Actions are pinned to full 40-character commit SHAs with version
1167
+ comments to prevent supply-chain attacks via mutable tag repointing.
1168
+
1147
1169
  Before enabling releases:
1148
1170
 
1149
1171
  1. Configure the GitHub repository as a RubyGems trusted publisher.
@@ -58,48 +58,8 @@
58
58
  return;
59
59
  }
60
60
 
61
- var coreFields = [
62
- "id", "title", "url", "excerpt", "content",
63
- "categories", "tags", "categoriesText", "tagsText",
64
- "date", "date_timestamp", "embedding"
65
- ];
66
-
67
- function normalize(entry) {
68
- var id = entry.id || entry.url;
69
- if (!id) {
70
- return null;
71
- }
72
- var categories = Array.isArray(entry.categories) ? entry.categories : [];
73
- var tags = Array.isArray(entry.tags) ? entry.tags : [];
74
- var normalized = {
75
- id: id,
76
- title: entry.title || "Untitled",
77
- url: entry.url || "#",
78
- excerpt: entry.excerpt || "",
79
- content: entry.content || "",
80
- date: entry.date || "",
81
- date_timestamp: Number(entry.date_timestamp) || 0,
82
- categories: categories,
83
- tags: tags,
84
- categoriesText: categories.join(" "),
85
- tagsText: tags.join(" ")
86
- };
87
- if (entry.source) {
88
- normalized.source = entry.source;
89
- }
90
- if (Array.isArray(entry.embedding) && entry.embedding.length > 0) {
91
- normalized.embedding = entry.embedding;
92
- }
93
- Object.keys(entry).forEach(function (key) {
94
- if (coreFields.indexOf(key) === -1 && key !== "source" && !(key in normalized)) {
95
- var value = entry[key];
96
- if (value !== null && value !== undefined && value !== "") {
97
- normalized[key] = value;
98
- }
99
- }
100
- });
101
- return normalized;
102
- }
61
+ var normalize = window.ClientSearchShared.normalize;
62
+ var coreFields = window.ClientSearchShared.coreFields;
103
63
 
104
64
  function buildIndex(data) {
105
65
  if (!Array.isArray(data)) {
@@ -41,39 +41,7 @@
41
41
  return key.replace(/_([a-z])/g, function (_, char) { return char.toUpperCase(); });
42
42
  }
43
43
 
44
- function normalize(entry) {
45
- var id = entry.id || entry.url;
46
- if (!id) {
47
- return null;
48
- }
49
- var categories = Array.isArray(entry.categories) ? entry.categories : [];
50
- var tags = Array.isArray(entry.tags) ? entry.tags : [];
51
- var normalized = {
52
- id: id,
53
- title: entry.title || "Untitled",
54
- url: entry.url || "#",
55
- excerpt: entry.excerpt || "",
56
- content: entry.content || "",
57
- date: entry.date || "",
58
- date_timestamp: Number(entry.date_timestamp) || 0,
59
- categories: categories,
60
- tags: tags,
61
- categoriesText: categories.join(" "),
62
- tagsText: tags.join(" ")
63
- };
64
- if (entry.source) {
65
- normalized.source = entry.source;
66
- }
67
- Object.keys(entry).forEach(function (key) {
68
- if (!(key in normalized) && key !== "embedding") {
69
- var value = entry[key];
70
- if (value !== null && value !== undefined && value !== "") {
71
- normalized[key] = value;
72
- }
73
- }
74
- });
75
- return normalized;
76
- }
44
+ var normalize = window.ClientSearchShared.normalize;
77
45
 
78
46
  function loadIndex(config) {
79
47
  if (sharedIndex && sharedDocuments) {
@@ -0,0 +1,57 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ /**
5
+ * ClientSearch shared utilities — used by the base runtime and the
6
+ * dropdown runtime to normalize search index entries into a uniform
7
+ * document format.
8
+ *
9
+ * Loaded before client-search-base.js and client-search-dropdown.js.
10
+ * Exposes window.ClientSearchShared.normalize(entry).
11
+ */
12
+
13
+ var CORE_FIELDS = [
14
+ "id", "title", "url", "excerpt", "content",
15
+ "categories", "tags", "categoriesText", "tagsText",
16
+ "date", "date_timestamp", "embedding"
17
+ ];
18
+
19
+ function normalize(entry) {
20
+ var id = entry.id || entry.url;
21
+ if (!id) {
22
+ return null;
23
+ }
24
+ var categories = Array.isArray(entry.categories) ? entry.categories : [];
25
+ var tags = Array.isArray(entry.tags) ? entry.tags : [];
26
+ var normalized = {
27
+ id: id,
28
+ title: entry.title || "Untitled",
29
+ url: entry.url || "#",
30
+ excerpt: entry.excerpt || "",
31
+ content: entry.content || "",
32
+ date: entry.date || "",
33
+ date_timestamp: Number(entry.date_timestamp) || 0,
34
+ categories: categories,
35
+ tags: tags,
36
+ categoriesText: categories.join(" "),
37
+ tagsText: tags.join(" ")
38
+ };
39
+ if (entry.source) {
40
+ normalized.source = entry.source;
41
+ }
42
+ if (Array.isArray(entry.embedding) && entry.embedding.length > 0) {
43
+ normalized.embedding = entry.embedding;
44
+ }
45
+ Object.keys(entry).forEach(function (key) {
46
+ if (CORE_FIELDS.indexOf(key) === -1 && key !== "source" && !(key in normalized)) {
47
+ var value = entry[key];
48
+ if (value !== null && value !== undefined && value !== "") {
49
+ normalized[key] = value;
50
+ }
51
+ }
52
+ });
53
+ return normalized;
54
+ }
55
+
56
+ window.ClientSearchShared = { normalize: normalize, coreFields: CORE_FIELDS };
57
+ })();
@@ -105,7 +105,7 @@ module Jekyll
105
105
  end
106
106
 
107
107
  def runtime_assets
108
- assets = ["assets/client-search-base.js", "assets/adapters/#{engine}.js"]
108
+ assets = ["assets/client-search-shared.js", "assets/client-search-base.js", "assets/adapters/#{engine}.js"]
109
109
  assets.concat(query_embedder_assets) if engine == "semantic" && embedding_enabled?
110
110
  assets << "assets/client-search-related.js" if related_enabled?
111
111
  assets << "assets/client-search-dropdown.js" if dropdown_enabled?
@@ -90,24 +90,30 @@ module Jekyll
90
90
 
91
91
  def build_scripts(configuration, prefix)
92
92
  scripts = []
93
- engine = configuration.engine_url
94
- if engine
95
- attrs = ["src=\"#{CGI.escapeHTML(engine)}\""]
96
- if configuration.engine_crossorigin
97
- crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
98
- attrs << "crossorigin=\"#{crossorigin}\""
99
- end
100
- if configuration.engine_sri
101
- integrity = CGI.escapeHTML(configuration.engine_sri)
102
- attrs << "integrity=\"#{integrity}\""
103
- end
104
- scripts << "<script #{attrs.join(' ')}></script>"
105
- end
93
+ engine = engine_script(configuration)
94
+ scripts << engine if engine
106
95
  scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
96
+ scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
107
97
  scripts << "<script src=\"#{prefix}/assets/client-search-dropdown.js\"></script>"
108
98
  scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
109
99
  scripts.map { |script| " #{script}" }.join("\n")
110
100
  end
101
+
102
+ def engine_script(configuration)
103
+ url = configuration.engine_url
104
+ return nil unless url
105
+
106
+ attrs = ["src=\"#{CGI.escapeHTML(url)}\""]
107
+ if configuration.engine_crossorigin
108
+ crossorigin = CGI.escapeHTML(configuration.engine_crossorigin)
109
+ attrs << "crossorigin=\"#{crossorigin}\""
110
+ end
111
+ if configuration.engine_sri
112
+ integrity = CGI.escapeHTML(configuration.engine_sri)
113
+ attrs << "integrity=\"#{integrity}\""
114
+ end
115
+ "<script #{attrs.join(' ')}></script>"
116
+ end
111
117
  end
112
118
  end
113
119
  end
@@ -69,6 +69,7 @@ module Jekyll
69
69
  scripts << engine if engine
70
70
  scripts << "<script src=\"#{prefix}/assets/search-runtime-config.js\"></script>"
71
71
  scripts.concat(embedder_scripts(configuration, prefix))
72
+ scripts << "<script src=\"#{prefix}/assets/client-search-shared.js\"></script>"
72
73
  scripts << "<script src=\"#{prefix}/assets/client-search-base.js\"></script>"
73
74
  scripts << "<script src=\"#{prefix}/assets/adapters/#{configuration.engine}.js\"></script>"
74
75
  scripts.map { |script| " #{script}" }.join("\n")
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module ClientSearch
5
- VERSION = "0.3.1"
5
+ VERSION = "0.3.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-client-search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup
@@ -48,6 +48,7 @@ files:
48
48
  - assets/client-search-base.js
49
49
  - assets/client-search-dropdown.js
50
50
  - assets/client-search-related.js
51
+ - assets/client-search-shared.js
51
52
  - assets/includes/related-articles.html
52
53
  - assets/layouts/post-with-related.html
53
54
  - assets/query-embedders/ollama-api.js