Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions .github/workflows/dependabot-secure-flow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ permissions:
issues: write

jobs:
auto-merge-to-securite:
auto-merge-to-security:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -42,34 +42,34 @@ jobs:
with:
fetch-depth: 0

- name: Ensure securite branch exists
- name: Ensure security branch exists
run: |
git fetch origin securite 2>/dev/null || git switch --create securite
git push origin securite || true
git fetch origin security 2>/dev/null || git switch --create security
git push origin security || true

- name: Merge dependabot changes to securite branch
- name: Merge dependabot changes to security branch
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'

# Fetch the PR branch
git fetch origin ${{ github.head_ref }}:${{ github.head_ref }} || true

# Switch to securite and merge
git switch securite
# Switch to security and merge
git switch security
git merge origin/${{ github.head_ref }} --no-edit || true

# Push to securite
git push origin securite
# Push to security
git push origin security

- name: Close and Delete Dependabot Branch
if: ${{ github.actor == 'dependabot[bot]' || startsWith(github.head_ref, 'dependabot/') }}
run: |
echo "Closing PR #${{ github.event.pull_request.number }} and deleting branch..."
gh pr close ${{ github.event.pull_request.number }} --delete-branch --comment "✅ Merged into **securite** branch for batch processing."
gh pr close ${{ github.event.pull_request.number }} --delete-branch --comment "✅ Merged into **security** branch for batch processing."

create-pr-to-main:
needs: auto-merge-to-securite
needs: auto-merge-to-security
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -78,7 +78,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: securite
ref: security
fetch-depth: 0

- name: Update Documentation Timestamp
Expand All @@ -102,29 +102,29 @@ jobs:
echo "No documentation changes needed."
else
git commit -m "docs: update release timestamp and changelog"
git push origin securite
git push origin security
fi

- name: Check if PR already exists
id: check-pr
run: |
# Target MAIN instead of master
PR_COUNT=$(gh pr list --base main --head securite --state open --json number | jq 'length')
PR_COUNT=$(gh pr list --base main --head security --state open --json number | jq 'length')
echo "pr_count=$PR_COUNT" >> $GITHUB_OUTPUT

- name: Create PR from securite to main
- name: Create PR from security to main
if: steps.check-pr.outputs.pr_count == '0'
run: |
git config --global user.name 'github-actions[bot]'
# Check commits between main and securite
NEW_COMMITS=$(git log main..securite --oneline | wc -l)
# Check commits between main and security
NEW_COMMITS=$(git log main..security --oneline | wc -l)

if [ "$NEW_COMMITS" -gt 0 ]; then
gh pr create \
--base main \
--head securite \
--head security \
--title "chore: dependency updates batch" \
--body "Automated dependency updates validated in the securite branch." \
--body "Automated dependency updates validated in the security branch." \
--label "dependencies" \
--label "automated" || echo "PR already exists"
fi
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-notification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:

\`\`\`yaml
jobs:
auto-merge-to-securite:
auto-merge-to-security:
uses: EthanThePhoenix38/dependabot-secure-flow/.github/workflows/dependabot-secure-flow.yml@${release.tag_name}
secrets: inherit
\`\`\`
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reader: removed third-party Google tag from iframe reader page
- Reader: keyword search now ignores language while matching and falls back to visible articles if no exact match
- Reader: category quick-tags added for one-click section anchor navigation
- Security: Tracker UUID generation now uses RFC4122 v4 with secure randomness (`crypto.getRandomValues`) when available
- Security: Legacy fallback for UUID randomness retained for environments without Web Crypto
- CI: Dependabot secure flow branch name normalized to `security` across auto-merge and auto-PR workflows
- Reader: EN language switch icon updated to a US-style flag logo (no emoji)
Comment on lines +27 to +30
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The PR title and description indicate this is a "dependency updates batch (security)" that includes "Automated dependency updates validated in the security branch", but this PR doesn't appear to include any actual dependency updates (no changes to package.json or package-lock.json). The changes are primarily code improvements (UUID generation with crypto.getRandomValues, workflow branch renaming, and CSS updates).

Consider either:

  1. Updating the PR title and description to accurately reflect that this contains security improvements and workflow changes rather than dependency updates, or
  2. Including the actual dependency updates that were validated in the security branch

Copilot uses AI. Check for mistakes.
- Stats: privacy-first tracker (localStorage + first-party session cookie)
- Portfolio: GitHub links point to the repositories tab (no viewer indirection)
- Privacy: fix header logo reference (`logo_text.png` -> `logo_final.png`)
Expand Down
7 changes: 7 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ Last updated: 2026-02-16

## Termine

- [x] Dependabot flow: unification du nom de branche en `security` (plus `securite`)
- Auto-merge, auto-commit et auto-PR aligns sur `security -> main`

- [x] Security: UUID session/visitor en v4 RFC4122 avec source aleatoire cryptographique quand disponible
- `crypto.getRandomValues` prioritaire
- Fallback legacy sur `Math.random` pour compatibilite

- [x] Reader UX: clic sur tag (mot-cle/categorie) pour ancrer vers section et eviter le scroll long
- Tags de navigation rapides (quick-nav) construits automatiquement depuis les sections visibles

Expand Down
71 changes: 20 additions & 51 deletions js/tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,27 +83,21 @@ const Tracker = {
},


/**
* -------------------------------------------------------------------------
* MÉTHODE : getSecureRandomValues()
* -------------------------------------------------------------------------
* Remplit un tableau typé avec des valeurs aléatoires.
*
* Utilise un générateur cryptographiquement sûr (crypto.getRandomValues)
* lorsqu'il est disponible, et retombe sur Math.random() sinon.
*/
// Fournit des octets aleatoires cryptographiquement forts quand disponible.
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The word "aleatoires" is missing its proper French accent. It should be "aléatoires" to maintain consistency with the rest of the codebase which uses proper French accents throughout (e.g., "MÉTHODE", "Récupère", "préférences").

Suggested change
// Fournit des octets aleatoires cryptographiquement forts quand disponible.
// Fournit des octets aléatoires cryptographiquement forts quand disponible.

Copilot uses AI. Check for mistakes.
// Fallback legacy: Math.random pour environnements sans Web Crypto.
getSecureRandomValues: function (typedArray) {
// Navigateur moderne ou environnement avec crypto.getRandomValues
var cryptoObj = (typeof globalThis !== 'undefined' && globalThis.crypto) ||
(typeof window !== 'undefined' && window.crypto) ||
(typeof self !== 'undefined' && self.crypto);
const cryptoObj = (
(typeof globalThis !== 'undefined' && globalThis.crypto) ||
(typeof window !== 'undefined' && window.crypto) ||
null
);

if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
return cryptoObj.getRandomValues(typedArray);
}

// Fallback : utiliser Math.random() (moins sûr, mais garantit le fonctionnement)
for (var i = 0; i < typedArray.length; i++) {
for (let i = 0; i < typedArray.length; i++) {
typedArray[i] = Math.floor(Math.random() * 256);
}
return typedArray;
Expand All @@ -113,47 +107,22 @@ const Tracker = {
* -------------------------------------------------------------------------
* MÉTHODE : generateUUID()
* -------------------------------------------------------------------------
* Génère un identifiant unique universel (UUID v4).
*
* EXEMPLE DE RÉSULTAT:
* "a1b2c3d4-e5f6-4789-a012-b34567890abc"
*
* FORMAT:
* xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
* - Le 4 indique la version 4 (basée sur des nombres aléatoires)
* - x = chiffre hexadécimal (0-9, a-f)
* - y = 8, 9, a, ou b (variante DCE)
*
* UTILISATION:
* Cet ID est stocké localement et sert à distinguer les visiteurs
* dans les statistiques locales. Il ne permet PAS d'identifier
* la personne car il n'est pas partagé avec des serveurs.
* Génère un identifiant unique universel (UUID v4) (RFC 4122).
*/
generateUUID: function () {
// Générer 16 octets aléatoires
var bytes = new Uint8Array(16);
this.getSecureRandomValues(bytes);

// Ajuster la version (4) et la variante (RFC 4122)
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variante 10xxxxxx

// Convertir en chaîne hexadécimale UUID
var hex = [];
for (var i = 0; i < bytes.length; i++) {
var h = bytes[i].toString(16);
if (h.length === 1) {
h = '0' + h;
}
hex.push(h);
}
const bytes = this.getSecureRandomValues(new Uint8Array(16));

// Version 4 (0100xxxx) et variante RFC 4122 (10xxxxxx)
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;

const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0'));
return (
hex[0] + hex[1] + hex[2] + hex[3] + '-' +
hex[4] + hex[5] + '-' +
hex[6] + hex[7] + '-' +
hex[8] + hex[9] + '-' +
hex[10] + hex[11] + hex[12] + hex[13] + hex[14] + hex[15]
hex.slice(0, 4).join('') + '-' +
hex.slice(4, 6).join('') + '-' +
hex.slice(6, 8).join('') + '-' +
hex.slice(8, 10).join('') + '-' +
hex.slice(10, 16).join('')
);
},

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 29 additions & 4 deletions readme-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,39 @@
}

.flag-fr { background: linear-gradient(90deg, #1e3a8a 0 33%, #ffffff 33% 66%, #dc2626 66% 100%); }
.flag-en { background: linear-gradient(90deg, #1e40af 0 100%); position: relative; }
.flag-en {
position: relative;
background:
repeating-linear-gradient(
to bottom,
#b91c1c 0px,
#b91c1c 1.08px,
#ffffff 1.08px,
#ffffff 2.16px
);
overflow: hidden;
}
.flag-en::before, .flag-en::after {
content: '';
position: absolute;
background: #ffffff;
display: block;
}
.flag-en::before {
left: 0;
top: 0;
width: 40%;
height: 54%;
background: #1e3a8a;
}
.flag-en::after {
left: 6%;
top: 8%;
width: 28%;
height: 38%;
background:
radial-gradient(circle, #ffffff 0 0.6px, transparent 0.7px) 0 0 / 4px 4px;
opacity: 0.9;
}
.flag-en::before { left: 45%; top: 0; width: 10%; height: 100%; }
.flag-en::after { top: 43%; left: 0; width: 100%; height: 14%; }

.sr-only {
position: absolute;
Expand Down
36 changes: 35 additions & 1 deletion tests/tracker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function createCookieJar() {
};
}

function loadTrackerIntoSandbox({ localStorage, cookieJar, fetchImpl, now }) {
function loadTrackerIntoSandbox({ localStorage, cookieJar, fetchImpl, now, cryptoImpl }) {
const trackerPath = path.join(process.cwd(), 'js', 'tracker.js');
const code = fs.readFileSync(trackerPath, 'utf8');

Expand All @@ -57,13 +57,16 @@ function loadTrackerIntoSandbox({ localStorage, cookieJar, fetchImpl, now }) {
setTimeout,
clearTimeout,
};
if (cryptoImpl) sandbox.crypto = cryptoImpl;

vm.createContext(sandbox);
vm.runInContext(code, sandbox, { filename: 'js/tracker.js' });

return sandbox;
}

const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

test('Premiere visite: cree une session et increment sessions/pageViews', async () => {
const localStorage = createLocalStorage();
const cookieJar = createCookieJar();
Expand Down Expand Up @@ -140,3 +143,34 @@ test('Throttle geoip: ne refait pas fetch si lastLocUpdate recent et locations p

assert.equal(fetchCalls, 0);
});

test('UUID: utilise crypto.getRandomValues quand disponible', async () => {
const localStorage = createLocalStorage();
const cookieJar = createCookieJar();
let called = 0;
const cryptoImpl = {
getRandomValues(arr) {
called++;
for (let i = 0; i < arr.length; i++) arr[i] = i;
return arr;
}
};
const fetchImpl = async () => ({ json: async () => ({ city: 'Grenoble', country_name: 'France' }) });

const sandbox = loadTrackerIntoSandbox({ localStorage, cookieJar, fetchImpl, now: 1000, cryptoImpl });
const uuid = sandbox.window.aiPulseTracker.generateUUID();

assert.ok(called > 0);
assert.match(uuid, UUID_V4_REGEX);
});

test('UUID: fallback Math.random si crypto indisponible', async () => {
const localStorage = createLocalStorage();
const cookieJar = createCookieJar();
const fetchImpl = async () => ({ json: async () => ({ city: 'Grenoble', country_name: 'France' }) });

const sandbox = loadTrackerIntoSandbox({ localStorage, cookieJar, fetchImpl, now: 1000 });
const uuid = sandbox.window.aiPulseTracker.generateUUID();

assert.match(uuid, UUID_V4_REGEX);
});