Skip to content

Commit 897f986

Browse files
authored
Merge pull request #38 from shiftleftcyber/blog-posts
Blog posts
2 parents 0edc33d + 006c78f commit 897f986

7 files changed

Lines changed: 380 additions & 6 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
+++
2+
author = "Jason Smith"
3+
title = "An Interesting and Useful Visualization for Security Teams"
4+
date = "2026-03-09"
5+
linkedin = "https://www.linkedin.com/posts/j28smith_an-interesting-and-useful-visualization-for-activity-7435683818319134720-Icis/"
6+
image = "img/thirdparty/2026-03-09-an-interesting-and-useful-visualization-for-security-teams.png"
7+
+++
8+
9+
Zero Day Clock: [https://zerodayclock.com/](https://zerodayclock.com/)
10+
11+
The Zero Day Clock tracks how quickly vulnerabilities move from disclosure to active exploitation across tens of
12+
thousands of CVEs. The trend is concerning: the time between discovery and weaponization continues to compress.
13+
14+
Worth a look if you work in software security, vulnerability management, or supply chain risk.
15+
16+
Thanks [Mihai (MM) Maruseac](https://www.linkedin.com/in/mihai-maruseac/) for sharing in the
17+
[OpenSSF](https://openssf.org/) Slack.
18+
19+
Happy Friday!
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
+++
2+
author = "Jason Smith"
3+
title = "🔀 Convergence in SBOM Signing"
4+
date = "2026-03-16"
5+
linkedin = "https://www.linkedin.com/posts/j28smith_cyclonedx-spdx-sbom-activity-7436868417690746881-Ywy6/"
6+
image = "img/thirdparty/2026-03-16-convergence-in-sbom-signing.png"
7+
+++
8+
9+
"Don't roll your own crypto." It's the first rule of security engineering, and it turns out it's also the best way to
10+
build a global standard. 🤝
11+
12+
The last few weeks of work on SBOM Signing Best Practices have been a masterclass in the power of open-source
13+
community collaboration. What started as a technical draft has evolved into real-time alignment between
14+
[CycloneDX](https://cyclonedx.org/) and [SPDX](https://spdx.dev/).
15+
16+
Here are the major updates and lessons learned:
17+
18+
## 1️⃣ From JSF to JSS (The CycloneDX Evolution)
19+
20+
While CycloneDX currently uses [JSF (JSON Signature Format)](https://cyberphone.github.io/doc/security/jsf.html), a
21+
conversation with [Steve Springett](https://www.linkedin.com/in/stevespringett/) led me to the authors of the specs,
22+
[Anders Rundgren](https://www.linkedin.com/in/andersrundgren/) and
23+
[Bret Jordan, MS, CISSP](https://www.linkedin.com/in/bretjordan/).
24+
25+
I learned JSF evolved into [JSS (JSON Signature Scheme)](https://www.itu.int/rec/T-REC-X.590/en), which was formally
26+
standardized by the [ITU](https://www.itu.int/) as [X.590](https://www.itu.int/rec/T-REC-X.590/en). With CycloneDX
27+
moving toward JSON-only in v2 later this year, it was the perfect time to suggest a move to this formal standard. Steve
28+
agreed, and a PoC is already in the works to make JSS the signature standard for the next generation of CycloneDX. 🚀
29+
30+
🔗 Track the Issue Here:
31+
[https://github.com/CycloneDX/specification/issues/851](https://github.com/CycloneDX/specification/issues/851)
32+
33+
## 2️⃣ Bringing Consistency to SPDX
34+
35+
Following a presentation to the [OpenSSF](https://openssf.org/) SBOM Everywhere SIG,
36+
[Kate Stewart](https://www.linkedin.com/in/katestewartaustin/) invited me to share these findings with the SPDX Tech
37+
Call. The feedback was fantastic and has led to new initiatives within the SPDX model:
38+
39+
SPDX is considering using JCS for underlying data consistency.
40+
41+
🔗 Track the Issue Here:
42+
[https://github.com/spdx/spdx-spec/issues/1362](https://github.com/spdx/spdx-spec/issues/1362)
43+
44+
SPDX is exploring JSS (X.590) as an option for introducing cryptographic signatures to the SPDX 3.0 model.
45+
46+
🔗 Track the Issue Here:
47+
[https://github.com/spdx/spdx-3-model/issues/1065](https://github.com/spdx/spdx-3-model/issues/1065#issuecomment-3953855076)
48+
49+
## 3️⃣ The Road to Formal Standardization (ITU)
50+
51+
A common concern with new specs is "Who owns this"? I'm excited to share that Bret Jordan is also leading an initiative
52+
to formally standardize JCS within the ITU.
53+
54+
Moving JCS to a formal ITU standard provides the regulatory-grade foundation that global enterprises and governments
55+
require for long-term supply chain trust.
56+
57+
## 🤔 Why This Matters: A Unified Path
58+
59+
The technical stars are aligning. By leveraging JSS and JCS, we are building a unified path for the industry.
60+
61+
**🎯 Core Support:** JCS is heavily used across many industries. It was recently added as a core function in Go with
62+
existing libraries available in many other languages, enabling dependency-light implementations.
63+
64+
**🔁 Interoperability:** This drives consistency between SPDX and CycloneDX, offering a standardized approach that
65+
works across the entire software supply chain.
66+
67+
**🙅‍♂️ No Custom Logic:** This approach leverages existing, supported international standards rather than
68+
"rolling our own".
69+
70+
A huge thank you to the open source community on the collaboration and the sanity checks on this journey.
71+
72+
The benchmark for SBOM integrity is being built right now. Are you ready for a standardized future?
73+
74+
\#SBOM #SupplyChainSecurity \#Cryptography \#JCS \#JSS

marketing/package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
},
1010
"author": "wamo <ainznino@pm.me>",
1111
"license": "MIT",
12-
"scripts": {
13-
"start": "concurrently npm:watch:*",
14-
"watch:tw": "tailwindcss -i ./themes/tella/assets/css/main.css -o ./themes/tella/assets/css/style.css --watch",
15-
"watch:hugo": "hugo server",
16-
"build": "tailwindcss -i ./themes/tella/assets/css/main.css -o ./themes/tella/assets/css/style.css && hugo --minify"
17-
},
12+
"scripts": {
13+
"start": "concurrently npm:watch:*",
14+
"watch:tw": "tailwindcss -i ./themes/tella/assets/css/main.css -o ./themes/tella/assets/css/style.css --watch",
15+
"watch:hugo": "hugo server",
16+
"build": "tailwindcss -i ./themes/tella/assets/css/main.css -o ./themes/tella/assets/css/style.css && hugo --minify",
17+
"import:linkedin": "node ./scripts/import-linkedin-post.mjs"
18+
},
1819
"devDependencies": {
1920
"@cyclonedx/cyclonedx-npm": "^4.1.2",
2021
"@tailwindcss/typography": "^0.5.12",

marketing/scripts/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# LinkedIn Post Importer
2+
3+
Create a Hugo blog post from a LinkedIn post without relying on LinkedIn read API access.
4+
5+
```sh
6+
npm run import:linkedin -- \
7+
--url "https://www.linkedin.com/posts/..." \
8+
--date 2026-04-29 \
9+
--title "My Post Title" \
10+
--text-file ./post.txt \
11+
--image ./post.png
12+
```
13+
14+
Use `--text "Post body"` instead of `--text-file` for short posts. The image argument is optional; when omitted, the post uses `img/default.jpg`.
15+
16+
The importer writes Markdown to `content/blog/YYYY-MM-DD-title-slug.md`, copies images to `static/img/thirdparty/`, and rejects duplicates unless `--overwrite` is supplied.
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env node
2+
3+
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
4+
import path from "node:path";
5+
import process from "node:process";
6+
7+
const CONTENT_DIR = path.resolve("content/blog");
8+
const IMAGE_DIR = path.resolve("static/img/thirdparty");
9+
const AUTHOR = "Jason Smith";
10+
const DEFAULT_IMAGE = "img/default.jpg";
11+
const IMAGE_EXTENSIONS = new Set([
12+
".gif",
13+
".jpeg",
14+
".jpg",
15+
".png",
16+
".svg",
17+
".webp",
18+
]);
19+
20+
function usage() {
21+
return `
22+
Usage:
23+
npm run import:linkedin -- --url <linkedin-url> --date <YYYY-MM-DD> --title <title> (--text <post> | --text-file <path>) [--image <path>] [--overwrite]
24+
25+
Examples:
26+
npm run import:linkedin -- --url "https://www.linkedin.com/posts/..." --date 2026-04-29 --title "My Post" --text-file ./post.txt --image ./post.png
27+
npm run import:linkedin -- --url "https://www.linkedin.com/posts/..." --date 2026-04-29 --title "My Post" --text "Post body"
28+
`;
29+
}
30+
31+
function parseArgs(argv) {
32+
const args = {};
33+
34+
for (let index = 0; index < argv.length; index += 1) {
35+
const arg = argv[index];
36+
37+
if (arg === "--help" || arg === "-h") {
38+
args.help = true;
39+
continue;
40+
}
41+
42+
if (arg === "--overwrite") {
43+
args.overwrite = true;
44+
continue;
45+
}
46+
47+
if (!arg.startsWith("--")) {
48+
throw new Error(`Unexpected positional argument: ${arg}`);
49+
}
50+
51+
const key = arg.slice(2);
52+
const value = argv[index + 1];
53+
54+
if (!value || value.startsWith("--")) {
55+
throw new Error(`Missing value for --${key}`);
56+
}
57+
58+
args[toCamelCase(key)] = value;
59+
index += 1;
60+
}
61+
62+
return args;
63+
}
64+
65+
function toCamelCase(value) {
66+
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
67+
}
68+
69+
function validateArgs(args) {
70+
const missing = [];
71+
72+
for (const key of ["url", "date", "title"]) {
73+
if (!args[key]) {
74+
missing.push(`--${key}`);
75+
}
76+
}
77+
78+
if (!args.text && !args.textFile) {
79+
missing.push("--text or --text-file");
80+
}
81+
82+
if (args.text && args.textFile) {
83+
throw new Error("Use either --text or --text-file, not both.");
84+
}
85+
86+
if (missing.length > 0) {
87+
throw new Error(`Missing required argument(s): ${missing.join(", ")}`);
88+
}
89+
90+
if (!/^https:\/\/(www\.)?linkedin\.com\//.test(args.url)) {
91+
throw new Error("--url must be a LinkedIn URL beginning with https://www.linkedin.com/");
92+
}
93+
94+
if (!/^\d{4}-\d{2}-\d{2}$/.test(args.date)) {
95+
throw new Error("--date must use YYYY-MM-DD format.");
96+
}
97+
98+
const parsedDate = new Date(`${args.date}T00:00:00Z`);
99+
if (Number.isNaN(parsedDate.getTime()) || parsedDate.toISOString().slice(0, 10) !== args.date) {
100+
throw new Error("--date is not a valid calendar date.");
101+
}
102+
}
103+
104+
function slugify(title) {
105+
const slug = title
106+
.normalize("NFKD")
107+
.replace(/[\u0300-\u036f]/g, "")
108+
.toLowerCase()
109+
.replace(/&/g, " and ")
110+
.replace(/[^a-z0-9]+/g, "-")
111+
.replace(/^-+|-+$/g, "")
112+
.replace(/-{2,}/g, "-");
113+
114+
if (!slug) {
115+
throw new Error("The title did not produce a usable slug.");
116+
}
117+
118+
return slug;
119+
}
120+
121+
function escapeTomlString(value) {
122+
return value
123+
.replace(/\\/g, "\\\\")
124+
.replace(/"/g, '\\"')
125+
.replace(/\r/g, "\\r")
126+
.replace(/\n/g, "\\n");
127+
}
128+
129+
function normalizeText(value) {
130+
const normalized = value
131+
.replace(/\r\n/g, "\n")
132+
.replace(/\r/g, "\n")
133+
.trim();
134+
135+
return normalized
136+
.split("\n")
137+
.map((line) => linkifyBareUrls(line.trimEnd()))
138+
.join("\n")
139+
.replace(/\n{3,}/g, "\n\n");
140+
}
141+
142+
function linkifyBareUrls(line) {
143+
return line.replace(/(^|[\s(])((https?:\/\/)[^\s<>()\]]+[^\s<>().,\];:'"!?])/g, (match, prefix, url) => {
144+
const beforeUrl = line.slice(0, line.indexOf(match) + prefix.length);
145+
146+
if (beforeUrl.endsWith("](") || beforeUrl.endsWith('"') || beforeUrl.endsWith("'")) {
147+
return match;
148+
}
149+
150+
return `${prefix}[${url}](${url})`;
151+
});
152+
}
153+
154+
async function readPostText(args) {
155+
if (args.textFile) {
156+
return readFile(path.resolve(args.textFile), "utf8");
157+
}
158+
159+
return args.text;
160+
}
161+
162+
async function resolveImage(args, slug, overwrite) {
163+
if (!args.image) {
164+
return DEFAULT_IMAGE;
165+
}
166+
167+
const sourcePath = path.resolve(args.image);
168+
const sourceStats = await stat(sourcePath);
169+
170+
if (!sourceStats.isFile()) {
171+
throw new Error(`Image path is not a file: ${args.image}`);
172+
}
173+
174+
const extension = path.extname(sourcePath).toLowerCase();
175+
if (!IMAGE_EXTENSIONS.has(extension)) {
176+
throw new Error(`Unsupported image extension "${extension}". Supported: ${[...IMAGE_EXTENSIONS].join(", ")}`);
177+
}
178+
179+
await mkdir(IMAGE_DIR, { recursive: true });
180+
181+
const imageFileName = `${slug}${extension}`;
182+
const destinationPath = path.join(IMAGE_DIR, imageFileName);
183+
184+
if (!overwrite && await exists(destinationPath)) {
185+
throw new Error(`Image already exists: ${path.relative(process.cwd(), destinationPath)}. Use --overwrite to replace it.`);
186+
}
187+
188+
await copyFile(sourcePath, destinationPath);
189+
190+
return `img/thirdparty/${imageFileName}`;
191+
}
192+
193+
async function exists(filePath) {
194+
try {
195+
await stat(filePath);
196+
return true;
197+
} catch (error) {
198+
if (error.code === "ENOENT") {
199+
return false;
200+
}
201+
202+
throw error;
203+
}
204+
}
205+
206+
function buildMarkdown({ title, date, url, image, body }) {
207+
return `+++
208+
author = "${AUTHOR}"
209+
title = "${escapeTomlString(title)}"
210+
date = "${date}"
211+
linkedin = "${escapeTomlString(url)}"
212+
image = "${escapeTomlString(image)}"
213+
+++
214+
215+
${body}
216+
`;
217+
}
218+
219+
async function main() {
220+
const args = parseArgs(process.argv.slice(2));
221+
222+
if (args.help) {
223+
process.stdout.write(usage());
224+
return;
225+
}
226+
227+
validateArgs(args);
228+
229+
const slug = `${args.date}-${slugify(args.title)}`;
230+
const postPath = path.join(CONTENT_DIR, `${slug}.md`);
231+
232+
if (!args.overwrite && await exists(postPath)) {
233+
throw new Error(`Post already exists: ${path.relative(process.cwd(), postPath)}. Use --overwrite to replace it.`);
234+
}
235+
236+
await mkdir(CONTENT_DIR, { recursive: true });
237+
238+
const rawText = await readPostText(args);
239+
const body = normalizeText(rawText);
240+
241+
if (!body) {
242+
throw new Error("Post body is empty after normalization.");
243+
}
244+
245+
const image = await resolveImage(args, slug, Boolean(args.overwrite));
246+
const markdown = buildMarkdown({
247+
title: args.title,
248+
date: args.date,
249+
url: args.url,
250+
image,
251+
body,
252+
});
253+
254+
await writeFile(postPath, markdown, "utf8");
255+
256+
process.stdout.write(`Created ${path.relative(process.cwd(), postPath)}\n`);
257+
process.stdout.write(`Image: ${image}\n`);
258+
}
259+
260+
main().catch((error) => {
261+
process.stderr.write(`Error: ${error.message}\n`);
262+
process.stderr.write(usage());
263+
process.exitCode = 1;
264+
});
138 KB
Loading
905 KB
Loading

0 commit comments

Comments
 (0)