Skip to content

feat(isms-change-management): add ISMS Change Management compliance GitHub Action - #59

Draft
yanjost wants to merge 3 commits into
mainfrom
feat/ICY-848/isms-change-management
Draft

feat(isms-change-management): add ISMS Change Management compliance GitHub Action#59
yanjost wants to merge 3 commits into
mainfrom
feat/ICY-848/isms-change-management

Conversation

@yanjost

@yanjost yanjost commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements a new composite GitHub Action isms-change-management that validates changes to ISMS documents (markdown files with YAML front matter) against the ISMS Change Management Policy.

Written in Ruby (allowed scripting language at Scalingo).

Checks performed

For each changed ISMS markdown file in a pull request, the action verifies:

# Check When
1 Author ≠ Validator — no person may appear in both authors and validators Always
2 Version bumpversion must be strictly greater than on the base branch (semver) When file exists on base
3 Double validation — minor or major version bumps require ≥ 2 validators When file exists on base
4 RSSI role — RSSI must validate; if RSSI is the author, CTO or CEO must validate instead When rssi input is set
5 Commit authors coherence — every git commit author touching the file must be listed in authors Always
6 PR reviewer coherencevalidators must exactly match the set of actual GitHub PR approvers (bidirectional) When github-token + pr-number are set
7 Version matches content change — the version bump level (patch/minor/major) must match the nature of the content change per the security policy When file exists on base

Content change classification (Check 7)

Classified as Rule
major > 50 % of non-empty body lines changed
minor a heading (#…) was added or removed
patch cosmetic / wording corrections only

A higher bump than required is always accepted; only bumping lower than the content warrants is an error.

PR comment

When github-token and pr-number are provided the action posts (or updates in place) a markdown comment on the PR with a ✅/❌ summary per check per file and an overall verdict.

Files added

isms-change-management/
  action.yml                         # Composite action definition
  README.md                          # Usage docs, check descriptions, PR comment examples
  scripts/check_isms_change.rb       # Ruby implementation
  tests/test_check_isms_change.rb    # 69 unit tests (minitest)
  tests/run.sh                       # Test runner

Files updated

  • .github/workflows/ci.yml — CI job running the Ruby test suite
  • .sclng/metadata.toml — added Ruby to languages list
  • .gitignore — ignore Python __pycache__ directories

Tests

69 unit tests covering all checks and edge cases.

69 runs, 140 assertions, 0 failures, 0 errors, 0 skips

Example usage

- uses: Scalingo/actions/isms-change-management@main
  with:
    base-ref: main
    rssi: "Yannick Jost"
    cto: "Léo Unbekandt"
    github-token: ${{ secrets.GITHUB_TOKEN }}
    pr-number: ${{ github.event.pull_request.number }}

Fix ICY-848

yanjost added 2 commits May 5, 2026 11:21
…ction

 Implements a new GitHub Action that validates changes to ISMS documents
 against the ISMS Change Management Policy (ISMS_CMPOL).

 Checks performed on each changed ISMS markdown file:
 1. Author ≠ Validator: no person may appear in both authors and validators
 2. Version bump: version must be strictly greater than on the base branch
 3. Double validation: minor or major bumps require at least 2 validators
 4. RSSI role (optional): RSSI must validate, unless RSSI is the author
    in which case CTO or CEO must validate instead

 Written in Ruby (allowed scripting language at Scalingo).
 Includes 29 unit tests covering all validation rules and edge cases.

 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…herence

 Add two new checks to the ISMS change management action:

 Check 5 - Commit authors coherence (no API required):
   Every git commit author touching an ISMS document must be listed
   in the document's authors field. Prevents undeclared authorship.

 Check 6 - PR reviewer coherence (optional, requires github-token + pr-number):
   YAML validators must exactly match the set of actual GitHub PR
   approvers (bidirectional). Prevents phantom validators and
   undocumented approvers.

 Also adds normalize_name/name_in_list? helpers for case-insensitive
 name matching across all checks.
@yanjost
yanjost requested a review from a team as a code owner May 5, 2026 09:58
@yanjost
yanjost requested review from SC-Jerome-S, leo-scalingo and matthieu526-scalingo and removed request for a team and matthieu526-scalingo May 5, 2026 09:58
end

def read_base_content(file, base_ref)
`git show "#{base_ref}:#{file}" 2>/dev/null`

@semgrep-code-scalingo semgrep-code-scalingo Bot May 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detected non-static command inside .... If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.

🧁 Fixed in commit d7e19ae 🧁

end

def changed_files(base_ref)
`git diff --name-only "#{base_ref}" HEAD 2>/dev/null`.split("\n").map(&:strip).reject(&:empty?)

@semgrep-code-scalingo semgrep-code-scalingo Bot May 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detected non-static command inside .... If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.

🧹 Fixed in commit d7e19ae 🧹

end

def commit_authors_for_file(file, base_ref)
`git log "#{base_ref}..HEAD" --format="%an" -- "#{file}" 2>/dev/null`

@semgrep-code-scalingo semgrep-code-scalingo Bot May 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detected non-static command inside .... If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.

Fixed in commit d7e19ae

end

def file_exists_in_base?(file, base_ref)
system("git cat-file -e \"#{base_ref}:#{file}\" 2>/dev/null")

@semgrep-code-scalingo semgrep-code-scalingo Bot May 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detected non-static command inside system. Audit the input to 'system'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.

🧹 Fixed in commit d7e19ae 🧹

…n policy, post PR comment

Add three new capabilities:

Check 7 — Version matches content change:
  Classifies each changed ISMS document body as patch/minor/major
  according to the security policy (>50% lines changed → major,
  heading added/removed → minor, cosmetic only → patch) and fails
  when the declared version bump is lower than the content warrants.

PR comment:
  When github-token + pr-number are provided, the action posts (or
  updates in place) a markdown comment on the PR summarising every
  check result per file with ✅/❌ emojis and an overall verdict.

Security fixes (semgrep):
  Replace shell-interpolated backtick/system calls with array-form
  IO.popen and system to eliminate shell injection risk. Two
  remaining findings (git revision-range and object-specifier
  arguments that must be a single token) are annotated with
  nosemgrep and an explanation.

Also add Python __pycache__ and *.pyc to .gitignore.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yanjost
yanjost force-pushed the feat/ICY-848/isms-change-management branch from f97f476 to 4c6b502 Compare May 5, 2026 11:21
leo-scalingo
leo-scalingo previously approved these changes May 28, 2026

@leo-scalingo leo-scalingo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a really complicated code to review (~1000 lines)

Have you been able to make the tests you wanted? Is it all green?

If yes LGTM, since I don't see any harmful code/shell command at first glance

@SC-Jerome-S
SC-Jerome-S requested a review from Copilot May 29, 2026 12:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new “ISMS Change Management Compliance” composite GitHub Action implemented in Ruby, with documentation, CI wiring, and a comprehensive Minitest suite to validate changes to ISMS Markdown documents (front matter + content) against the policy.

Changes:

  • Introduces isms-change-management composite action + Ruby validator script with multiple checks (authors/validators, versioning, roles, PR approvers, content-change classification).
  • Adds Minitest test suite and a test runner script; wires tests into CI.
  • Updates action documentation and repo metadata to reflect the new Ruby-based action.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
isms-change-management/scripts/check_isms_change.rb Core implementation of the ISMS change validation and optional PR comment posting.
isms-change-management/tests/test_check_isms_change.rb Unit tests covering the script helpers and checks.
isms-change-management/tests/run.sh Convenience script to run the Ruby test suite.
isms-change-management/action.yml Composite action definition exposing inputs and running the Ruby script.
isms-change-management/README.md Full documentation of checks, inputs, and example PR comment output.
.github/workflows/ci.yml Adds a CI job to run the action’s Ruby tests.
.sclng/metadata.toml Updates declared languages to include Ruby.
.gitignore Adds Python cache ignores (unrelated to the Ruby action).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +171 to +176
def check_author_validator_coherence(file, front_matter)
authors = Array(front_matter["authors"]).map(&:to_s)
validators = Array(front_matter["validators"]).map(&:to_s)

overlap = authors & validators
return [] if overlap.empty?
Comment on lines +236 to +243
def check_rssi_role(file, front_matter, rssi_names, cto_names, ceo_names)
return [] if rssi_names.empty?

authors = Array(front_matter["authors"]).map(&:to_s)
validators = Array(front_matter["validators"]).map(&:to_s)

rssi_is_author = (rssi_names & authors).any?
rssi_is_validator = (rssi_names & validators).any?
Comment on lines +134 to +148
def bump_kind(old_ver, new_ver)
parse = lambda do |v|
m = v.to_s.match(/\A(\d+)\.(\d+)\.(\d+)/)
m ? [m[1].to_i, m[2].to_i, m[3].to_i] : [0, 0, 0]
end

old = parse.call(old_ver)
new_v = parse.call(new_ver)

return :major if new_v[0] > old[0]
return :minor if new_v[1] > old[1]
return :patch if new_v[2] > old[2]

:none
end
Comment on lines +472 to +477
github_token = ENV.fetch("GITHUB_TOKEN", "").strip
pr_number = ENV.fetch("PR_NUMBER", "").strip
repository = ENV.fetch("GITHUB_REPOSITORY", "").strip

all_changed = changed_files(base_ref)
isms_files = all_changed.select { |f| f.end_with?(".md") && matches_pattern?(f, files_pattern) }

puts "Checking #{isms_files.size} changed ISMS document(s) against base #{base_ref}..."

can_post_comment = !github_token.empty? && !pr_number.empty? && !repository.empty?
Comment on lines +199 to +205
def check_double_validation(file, old_front_matter, new_front_matter)
old_version = old_front_matter["version"].to_s
new_version = new_front_matter["version"].to_s
validators = Array(new_front_matter["validators"]).map(&:to_s).reject(&:empty?)

kind = bump_kind(old_version, new_version)
return [] unless %i[minor major].include?(kind)
@yanjost
yanjost dismissed leo-scalingo’s stale review July 27, 2026 16:50

Dismissing approval — known bugs identified in the ISMS Change Management action; re-testing against real specifications PRs before re-requesting review.

@yanjost

yanjost commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Testing against real specifications PRs — 3 issues found

I ran check_isms_change.rb directly against real branches/PRs from Scalingo/specifications (not just the unit test suite). Converting this PR to draft until these are fixed.

1. (Critical) Front matter parsing silently fails on ~92% of real ISMS docs → false "all checks passed"

parse_front_matter uses YAML.safe_load(yaml_block, permitted_classes: []), which raises Psych::DisallowedClass on any YAML-native Date value (date:, creation_date:, modification_date: — present in virtually every ISMS doc's front matter). The rescue Psych::Exception swallows this and returns nil; the caller treats nil as "not an ISMS doc, skip silently" with no warning.

  • Verified: 49 of 53 files under isms/**/*.md in Scalingo/specifications fail this parse.
  • Concretely, on Scalingo/specifications#547 (isms/Information-Systems-Security-Policy-Fr.md), the front matter lists Jérôme Senelier and Yannick Jost in both authors and validators — a direct Check 1 violation — yet the action printed All ISMS change management checks passed. because the file was silently skipped.
  • Fix: permit Date/Time classes in YAML.safe_load, or normalize date-like fields to strings before parsing.

2. (Critical) Wrong default base branch assumption

resolve_base_ref falls back to origin/main when base-ref isn't explicitly passed. Scalingo/specifications's default branch is master. Without an explicit base-ref: master input, the script computes an empty/invalid diff and reports No ISMS documents changed — nothing to check. — a full false pass even when the PR changed ISMS files. Any caller workflow that omits base-ref gets a silent no-op gate.

3. (Medium) Changed-files diff uses two-dot instead of merge-base semantics

changed_files runs git diff base_ref HEAD directly instead of diffing from the merge-base. On specifications#547 this pulled in 3 unrelated files (AI-Security-Policy-Fr.md, BackupPolicy.md, SecNumCloud-Whitelisting-Procedure-En.md) that changed on master after the branch diverged but were never touched by the PR — the action ends up checking/reporting on files outside the actual PR diff.


Marking as draft until these are addressed — the 69 unit tests pass, but they don't exercise real front matter (with dates) or the real base-branch/diff scenario, which is how all three issues slipped through.

@yanjost
yanjost marked this pull request as draft July 28, 2026 08:33
@EtienneM

Copy link
Copy Markdown
Member

@yanjost is this PR still under your radar?

base-ref: main
rssi: "Yannick Jost"
cto: "Léo Unbekandt"
ceo: "Frédéric Harper"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wut?

@SC-Jerome-S

Copy link
Copy Markdown

@EtienneM I think yes, but it was postponed due to other topics
Sorry for the pending PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants