Skip to content

Release/v4#11

Merged
Tanishq1030 merged 2 commits intomainfrom
release/v4
Mar 23, 2026
Merged

Release/v4#11
Tanishq1030 merged 2 commits intomainfrom
release/v4

Conversation

@Tanishq1030
Copy link
Copy Markdown
Member

PR Release: v4.1.4 — Multi-ID Reporting & Windows Compatibility

This release focuses on refining Anchor's reporting architecture to support multi-compliance ID aggregation and ensuring a seamless experience for Windows users.

Changes

Multi-ID Compliance Reporting

  • Implemented Multi-ID consolidation in the scanning engine. A single violation now reports all applicable IDs from different sectors (e.g., [FINOS-014, SEC-007]) instead of creating redundant entries.
  • Refactored loader.py to eliminate "virtual rule" duplication, ensuring each pattern is scanned exactly once.

Canonical Rule Mapping

  • Remapped all detection patterns in mitigation.anchor to Canonical Domain IDs (SEC, ALN, etc.).
  • Updated Frameworks (FINOS, OWASP, NIST) and Regulators (RBI, EU AI Act, etc.) to use the maps_to relation for reporting.

Windows Compatibility Fixes

  • Replaced all problematic Unicode symbols (anchor, checkmarks, horizontal bars) with ASCII-safe equivalents in cli.py.
  • Resolved UnicodeEncodeError that prevented anchor init and anchor sync from completing on Windows systems.

Versioning & Integrity

  • Bounced version to 4.1.4.
  • Updated constitution.py with the new mitigation.anchor SHA-256 integrity hash.

Verification

  • Successfully verified consolidated ID headers using anchor check test_vuln.py --all.
  • Verified error-free anchor init --all on Windows environment.

Copilot AI review requested due to automatic review settings March 23, 2026 03:24
@Tanishq1030 Tanishq1030 merged commit 14ea4e6 into main Mar 23, 2026
2 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Release PR for Anchor v4.1.4 that updates the governance catalog and scanning/reporting flow to (1) consolidate multiple compliance IDs into a single reported violation, and (2) improve Windows terminal compatibility by removing some Unicode CLI glyphs.

Changes:

  • Bumped version to 4.1.4 across package metadata and CLI.
  • Updated governance mapping: mitigation patterns now target canonical domain IDs and frameworks/regulators map via maps_to.
  • Refactored alias handling and updated engine reporting to aggregate mapped IDs into a single violation entry.

Reviewed changes

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

Show a summary per file
File Description
setup.py Bumps package version to 4.1.4.
anchor/init.py Updates library __version__ to 4.1.4.
anchor/cli.py Updates init/sync output formatting, injects maps_to into exported rule dicts, changes rule loading/retention, and removes virtual-alias registration.
anchor/core/constitution.py Updates MITIGATION_SHA256 for integrity verification.
anchor/core/loader.py Refactors alias-chain construction and adds maps_to relations into alias chain.
anchor/core/engine.py Implements Multi-ID aggregation for regex and AST rule matches and updates suppression ignore IDs for subprocess calls.
anchor/governance/mitigation.anchor Remaps mitigation rule IDs to canonical domains and adjusts/extends patterns.
.anchor/constitution.anchor Updates manifest version and legacy alias mapping format.
.anchor/mitigation.anchor Mirrors mitigation catalog changes into the committed .anchor/ copy.
.anchor/reports/governance_audit.md Updates committed audit report to reflect new Multi-ID reporting output.
.anchor/frameworks/FINOS_Framework.anchor Adds FINOS framework mapping rules via maps_to.
.anchor/frameworks/OWASP_LLM.anchor Adds OWASP LLM Top 10 mapping rules via maps_to.
.anchor/frameworks/NIST_AI_RMF.anchor Adds NIST AI RMF mapping rules via maps_to.
.anchor/government/SEC_Regulations.anchor Adds SEC regulator mapping rules via maps_to.
.anchor/government/SEBI_Regulations.anchor Adds SEBI regulator mapping rules via maps_to.
.anchor/government/FCA_Regulations.anchor Adds FCA regulator mapping rules via maps_to.
.anchor/government/EU_AI_Act.anchor Adds EU AI Act mapping rules via maps_to.
.anchor/government/CFPB_Regulations.anchor Adds CFPB mapping rules via maps_to.
.anchor/.anchor.sig Removes legacy local signature file.
Comments suppressed due to low confidence (2)

anchor/core/engine.py:396

  • In the AST-based path, the inline suppression check still only looks for # anchor: ignore {rule['id']} (canonical ID). After introducing Multi-ID aggregation (framework/regulator IDs via maps_to), suppressing via a mapped ID (e.g., # anchor: ignore FINOS-014) will work for regex-based rules but not for AST-based rules. Make the AST suppression check consider the aggregated ID set consistently.
                            # Aggregate IDs (Canonical + Frameworks)
                            matching_ids = [rule['id']]
                            if hasattr(self, 'rules'):
                                for other in self.rules:
                                    if other.get('maps_to') == rule['id']: matching_ids.append(other['id'])
                            v_id = ", ".join(sorted(list(set(matching_ids))))

                            violations.append({
                                "id": v_id,
                                "name": rule.get("name", "Unnamed Rule"),
                                "description": rule.get("description", "No description provided."),
                                "message": rule.get("message", "Policy Violation"),
                                "mitigation": rule.get("mitigation", "No mitigation provided."),
                                "file": file_path,
                                "line": line_num,
                                "severity": rule.get("severity", "error")
                            })

anchor/cli.py:320

  • The policy_template written by anchor init no longer contains the YAML structure that the check command’s PolicyLoader expects (it reads config.get('rules', [])). With the current template, users won’t be able to define rule overrides because the file uses custom_rules: (and has an orphan-indented example block) instead of a top-level rules: list. Update the template to emit valid YAML with a rules: list (and, if needed, separate exclude: etc.) that matches what PolicyLoader merges.
        policy_template = f'''# =============================================================================
# {policy_name.replace('.anchor', '').upper()} — Project Policy
# =============================================================================
# This file is for YOUR project-specific rules.
# Automatically ignored by git to protect company policies.
#
# RULES:
#   1. Can only RAISE severity (ERROR -> BLOCKER is allowed)
#   2. Cannot LOWER severity — the floor is absolute
#   3. Cannot suppress constitutional rules
  # Example: raise SEC-006 from error to blocker
  # - id: SEC-006
  #   severity: blocker
  #   reason: >
  #     Our PCI-DSS scope requires blocking all direct LLM API calls.

custom_rules:
  # Example: add a company-specific rule
  # - id: INTERNAL-001
  #   name: Internal vault access pattern
  #   severity: blocker
  #   detection:
  #     method: regex
  #     pattern: 'vault\\.read\\((?!approved_keys)'
  #   description: >
  #     Vault read operations must only access approved_keys namespace.
'''

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

# Only fire on bulk access or sensitive key names
pattern: >-
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.environ\b(?!\s*\.get|["'])
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(?i)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This regex uses an inline (?i) flag in the middle of the pattern. In Python re, global inline flags must appear at the start of the expression (or use a scoped group like (?i:...)), otherwise re.error is raised and the rule will silently stop detecting anything. Rewrite the pattern to use a valid case-insensitive construct (e.g., move (?i) to the beginning or change to (?i:...)).

Suggested change
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(?i)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ)
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(?i:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ)

Copilot uses AI. Check for mistakes.
# Only fire on bulk access or sensitive key names
pattern: >-
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.environ\b(?!\s*\.get|["'])
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(?i)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This regex uses an inline (?i) flag in the middle of the pattern. In Python re, global inline flags must appear at the start of the expression (or use a scoped group like (?i:...)), otherwise re.error is raised and the rule will silently stop detecting anything. Rewrite the pattern to use a valid case-insensitive construct (e.g., move (?i) to the beginning or change to (?i:...)).

Suggested change
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(?i)(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API).*\]|\{\*\*os\.environ)
^(?:[^"\'#]|(["\'])(?:(?!\1).|\\\1)*\1)*\bos\.(environ\.(copy|items)\(\)|\benviron\b\s*\[.*(?i:(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API)).*\]|\{\*\*os\.environ)

Copilot uses AI. Check for mistakes.


from anchor import __version__
__version__ = "4.1.4"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

__version__ is imported from anchor and then immediately overwritten with a hard-coded string. This can drift from anchor/__init__.py and setup.py and makes the source of truth unclear. Prefer relying on the package anchor.__version__ (or a single version constant) and remove the reassignment here.

Suggested change
__version__ = "4.1.4"

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +272
# Aggregate IDs (Canonical + active Frameworks/Regulators)
matching_ids = [rule['id']]
if hasattr(self, 'rules'):
for other in self.rules:
if other.get('maps_to') == rule['id']: matching_ids.append(other['id'])
v_id = ", ".join(sorted(list(set(matching_ids))))
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The Multi-ID aggregation loops over self.rules for every match to find maps_to aliases. With larger rule sets this becomes O(matches × rules) and can significantly slow scans. Consider precomputing a reverse index once (e.g., maps_to -> [ids]) during engine initialization and using it here.

Copilot uses AI. Check for mistakes.
Comment on lines 415 to +441
@@ -428,45 +424,21 @@ def resolve_path(rel_path: str) -> Path:
visited.add(next_id)

if next_id in constitution.rules:
rule = constitution.rules[next_id]
# Update max severity
if severity_gte(rule.severity, max_severity):
max_severity = rule.severity

# Treat this rule as our canonical alias target
canonical_id = next_id
break # End of chain
# Target rule found!
constitution.alias_chain[alias_id] = next_id
break
elif next_id in manifest.legacy_aliases:
next_id = manifest.legacy_aliases[next_id]
else:
# Target not found in rules or aliases
break

if canonical_id and canonical_id in constitution.rules:
constitution.alias_chain[alias_id] = canonical_id

# Create virtual rule
target = constitution.rules[canonical_id]
alias_rule = Rule(
id=alias_id,
name=target.name,
namespace=target.namespace,
severity=max_severity, # Use inherited max severity
min_severity=target.min_severity,
description=target.description,
category=target.category,
maps_to=canonical_id,
obligation_type=target.obligation_type,
anchor_mechanism=target.anchor_mechanism,
source_file=target.source_file,
original_id=target.original_id,
v3_id=target.v3_id,
)
constitution.rules[alias_id] = alias_rule
else:
constitution.errors.append(
f"Could not resolve alias chain: {alias_id} → {target_id}"
)

# Also include maps_to relations from frameworks/regulators in the alias chain
for rid, rule in constitution.rules.items():
if rule.maps_to and rule.maps_to in constitution.rules:
# If a rule maps to another (e.g. FINOS-014 -> SEC-007)
# we treat it as an alias for reporting purposes
if rid not in constitution.alias_chain:
constitution.alias_chain[rid] = rule.maps_to
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

alias_chain entries for legacy IDs are now only set to the first rule found in the chain (e.g., ANC-* -> FINOS-*), even though FINOS rules themselves can maps_to a canonical domain rule. Because get_rule() only resolves one hop via alias_chain, looking up ANC-* will return the framework rule instead of the canonical domain rule described in the docstring. Consider resolving/storing the final canonical target (following maps_to/aliases transitively), or update get_rule() to follow alias_chain until it reaches a stable canonical ID.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +5
**Timestamp:** 2026-03-22 19:59:07
**Source:** `D:\Anchor\anchor\__init__.py`
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This committed audit report contains environment-specific, absolute Windows paths (e.g., D:\Anchor\...) and a run timestamp, which will cause noisy diffs and can leak local filesystem layout. Consider generating reports at runtime only (and adding them to .gitignore), or sanitize to relative paths and avoid embedding volatile timestamps in tracked files.

Suggested change
**Timestamp:** 2026-03-22 19:59:07
**Source:** `D:\Anchor\anchor\__init__.py`
**Timestamp:** (generated at runtime; omitted from committed report)
**Source:** `anchor/__init__.py`

Copilot uses AI. Check for mistakes.

click.echo("")
click.secho("Anchor V4 init", fg="cyan", bold=True)
click.secho("Anchor V4 - init", fg="cyan", bold=True)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This PR replaces some Unicode status symbols with ASCII, but cli.py still emits Unicode (e.g., in the unknown domain/framework/regulator error messages). On Windows terminals with a non-UTF8 code page this can still trigger UnicodeEncodeError. Consider converting the remaining symbols to ASCII as well (or enforcing UTF-8 output explicitly).

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +283
# Aggregate IDs (Canonical + active Frameworks/Regulators)
matching_ids = [rule['id']]
if hasattr(self, 'rules'):
for other in self.rules:
if other.get('maps_to') == rule['id']: matching_ids.append(other['id'])
v_id = ", ".join(sorted(list(set(matching_ids))))

if self.allow_suppressions:
if f"# anchor: ignore {rule.get('id')}" in match_text or "# anchor: ignore-all" in match_text:
if any(f"# anchor: ignore {rid}" in match_text for rid in matching_ids) or "# anchor: ignore-all" in match_text:
author = self._get_suppression_author(file_path, line_num)
suppressed.append({
"id": rule["id"], "name": rule.get("name"), "file": file_path, "line": line_num, "author": author, "severity": rule.get("severity", "error")
"id": v_id, "name": rule.get("name"), "file": file_path, "line": line_num, "author": author, "severity": rule.get("severity", "error")
})
is_suppressed = True
if not is_suppressed:
violations.append({
"id": rule["id"], "name": rule.get("name"), "description": rule.get("description"), "message": rule.get("message"), "mitigation": rule.get("mitigation"), "file": file_path, "line": line_num, "severity": rule.get("severity", "error")
"id": v_id, "name": rule.get("name"), "description": rule.get("description"), "message": rule.get("message"), "mitigation": rule.get("mitigation"), "file": file_path, "line": line_num, "severity": rule.get("severity", "error")
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

Multi-ID aggregation is currently stored by overwriting the violation id field with a comma-separated string. Downstream code (e.g., anchor.core.healer.suggest_fix() which does rule_id.startswith(prefix)) assumes id is a single rule identifier, so suggestions/auto-fixes and any ID-based lookups will stop working. Keep id as the canonical rule ID and add a separate field (e.g., ids: [...] or related_ids: [...]) for the aggregated set.

Copilot uses AI. Check for mistakes.
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.

2 participants