A Security Scanner Is Easy. Building One You Can Actually Trust Is Not.
I set out to build a repository security analyzer in C++17 without third-party runtime dependencies. The interesting part wasn't detecting system() or a hardcoded secret. It was turning a collection of scanners into one tool that could understand a repository, explain its risk, recommend what to do next, produce machine-readable reports, and fail a CI pipeline when the security policy was violated.
For a hackathon, it would have been easy to stop at:
Found a vulnerability.
I didn't want that.
I wanted the output to answer:
What is in this repository?
What is risky?
Where is it?
How serious is it?
What does it depend on?
What should I do about it?
Can another tool consume the result?
Can CI automatically reject it?
That became RepoShield.
And the zero-dependency constraint made the implementation much more interesting.
The constraint
The project had one constraint that influenced almost every architectural decision:
Build the core tool using C++17 and the standard library rather than relying on third-party runtime libraries.
That sounds simple until you list what a developer security tool actually needs.
A repository analyzer needs to deal with:
- filesystem traversal
- source-file discovery
- code structure
- security rules
- dependency analysis
- risk scoring
- report generation
- Git information
- remediation
- configuration
- CI integration
Normally, several of those problems are solved by installing a package.
For RepoShield, I kept asking a different question:
Do I actually need a dependency here, or do I need a small, well-defined piece of functionality?
That distinction shaped the project.
What RepoShield actually does
RepoShield is a standalone C++17 CLI for repository intelligence and security analysis.
The pipeline looks like this:
Repository
↓
File Discovery
↓
Repository Statistics + Code Lens + Dependency Analysis
↓
Security Analysis
↓
Risk Scoring
↓
Remediation + Reporting
↓
Policy Enforcement
↓
Exit 0 / 1
The important thing is that these aren't independent commands pretending to be one product.
The results flow through the system.
First problem: understanding the repository
Before detecting security issues, RepoShield has to understand what it is looking at.
The file scanner recursively walks the target repository using the C++17 filesystem facilities.
From there, repository statistics are calculated:
- Files
- Source files
- Header files
- Total size
- Total lines
- Languages
That might sound like basic information.
It is.
And that is intentional.
A security analyzer should not need a separate tool just to answer how large the repository is.
Code Lens: security tools shouldn't be blind to code structure
The next question was:
Can we understand a little more than files and line numbers without building a complete compiler?
I didn't try to build a C++ compiler frontend.
Instead, RepoShield's Code Lens extracts useful structural information from the source:
- Includes
- Functions
- Classes
- Structs
- Function locations
- Function sizes
- Class method counts
A scan can produce information such as:
FUNCTIONS
- process
- calculate
- unsafeCopy
- executeCommand
- main
CLASSES
- DemoProcessor
STRUCTS
- UserInfo
This is deliberately smaller than a full language parser.
The goal isn't to understand every semantic detail of C++.
The goal is to provide useful repository intelligence while keeping the tool lightweight.
Then comes the security analyzer
This is where RepoShield becomes a security tool rather than a repository statistics program.
The analyzer currently contains rules for several common security-sensitive patterns:
- RS001 — Unsafe C string function
- RS002 — Command execution detected
- RS003 — Possible hardcoded secret
- RS004 — Weak cryptographic algorithm
- RS005 — Potentially dangerous file operation
- RS006 — Potential SQL injection
- RS007 — Insecure random number generation
Each finding carries information such as:
- Rule ID
- Severity
- File
- Line
- Title
- Description
For example:
[RS001] Unsafe C string function
Severity: HIGH
File: demo-target/vulnerable.cpp
Another example:
[RS003] Possible hardcoded secret
Severity: CRITICAL
File: demo-target/vulnerable.cpp
That distinction matters.
A scanner saying:
Secret found.
forces the developer to do the rest of the investigation.
RepoShield tries to provide the context immediately.
The seven security issues
The vulnerable demonstration repository intentionally contains examples for all seven security rules.
RS001 — Unsafe C String Function
Unsafe C string operations can introduce buffer overflows when used incorrectly.
For example:
strcpy(buffer, input);
RepoShield reports:
[RS001] Unsafe C string function
Severity: HIGH
RS002 — Command Execution
The analyzer also detects command execution:
std::system(command);
RepoShield reports:
[RS002] Command execution detected
Severity: HIGH
RS003 — Possible Hardcoded Secret
Hardcoded credentials and API keys are another common problem.
For example:
const char* api_key = "sk_test_123456789abcdef";
RepoShield reports:
[RS003] Possible hardcoded secret
Severity: CRITICAL
RS004 — Weak Cryptographic Algorithm
RepoShield identifies references to weak cryptographic algorithms and reports:
[RS004] Weak cryptographic algorithm
Severity: HIGH
RS005 — Potentially Dangerous File Operation
For example:
std::remove("important_file.txt");
RepoShield reports:
[RS005] Potentially dangerous file operation
Severity: MEDIUM
RS006 — Potential SQL Injection
RepoShield also looks for suspicious SQL construction patterns.
For example:
std::string query = "SELECT * FROM users WHERE username = '" + username + "'";
The scanner reports:
[RS006] Potential SQL injection
Severity: HIGH
RS007 — Insecure Random Number Generation
Finally, RepoShield checks for insecure random number generation patterns.
For example:
std::rand();
The result:
[RS007] Insecure random number generation
Severity: MEDIUM
The seven rules cover different types of security-sensitive code and give the analyzer a broader security surface than a single vulnerability check.
A finding count is not a risk model
One of the first things I didn't want to do was treat every finding equally.
Imagine two repositories:
Repository A
10 LOW findings
Repository B
1 CRITICAL finding
A raw issue count makes Repository A look worse.
That doesn't make much security sense.
So RepoShield has a risk-scoring layer that considers finding severity.
The vulnerable demonstration repository produces:
Risk Level: CRITICAL
Risk Score: 100 / 100
Critical: 1
High: 4
Medium: 2
Low: 0
The point isn't that 100/100 is some universal security standard.
It is RepoShield's own normalized risk model.
The important architectural decision is separating:
Detection
from:
Risk interpretation
That means security rules can identify problems while another component decides how those problems contribute to repository-level risk.
One vulnerable repository, seven findings
I wanted the demo to prove that the rules weren't just theoretical.
So I created an intentionally vulnerable target containing examples for all seven rules.
Running:
./reposhield analyze demo-target
produces:
Issues found: 7
[RS001] Unsafe C string function
Severity: HIGH
[RS002] Command execution detected
Severity: HIGH
[RS003] Possible hardcoded secret
Severity: CRITICAL
[RS004] Weak cryptographic algorithm
Severity: HIGH
[RS005] Potentially dangerous file operation
Severity: MEDIUM
[RS006] Potential SQL injection
Severity: HIGH
[RS007] Insecure random number generation
Severity: MEDIUM
This was one of the most important parts of the project.
The demo isn't simply printing seven predefined messages.
The vulnerable source contains patterns that the actual analyzer detects.
That makes the demonstration reproducible.
The analyzer detects all seven configured security rules from the source code and then passes those findings to the risk-scoring and policy layers.
The supply-chain view
Security isn't only about the code you wrote.
It is also about what the code depends on.
RepoShield therefore analyzes source-level dependencies.
For example, a repository can expose dependencies such as:
[STANDARD] cstring[STANDARD] cstdlib[STANDARD] iostream[STANDARD] cstdio[STANDARD] string[EXTERNAL] openssl/sha.h
This creates an important distinction.
RepoShield itself can be built around the C++17 standard library while still analyzing repositories that use external dependencies.
The dependency analyzer is describing the target repository's dependency surface.
It isn't importing those dependencies into RepoShield.
Turning dependencies into a graph
Once dependency relationships existed as data, representing them as a graph became straightforward.
The same repository can be represented as:
demo-target/vulnerable.cpp
├── [STANDARD] cstring
├── [STANDARD] cstdlib
├── [STANDARD] iostream
├── [STANDARD] cstdio
├── [STANDARD] string
└── [EXTERNAL] openssl/sha.h
The graph isn't there just to look impressive in a terminal.
It gives the dependency analysis a structure that can later be consumed by other reporting or visualization layers.
The underlying lesson was simple:
Design the data model before deciding which library should represent it.
Detection without remediation is only half a workflow
At this point, RepoShield could tell me what was wrong.
But the next question was obvious:
What should the developer do now?
So security findings can be mapped to remediation guidance.
For example:
[RS001] Unsafe C string function
Recommendation:
Prefer bounds-checked string handling or safer C++ string operations and validate input sizes.
For command execution:
[RS002] Command execution detected
Recommendation:
Avoid executing untrusted input through system(). Validate input and prefer a strictly controlled command interface or allowlist.
For the hardcoded-secret rule:
Remove credentials from source code. Store secrets in environment variables or a dedicated secret-management mechanism.
The intended flow became:
Detect → Explain → Recommend
rather than simply:
Detect
Automatic remediation
RepoShield also contains a remediation engine for supported automatic fixes.
The CLI exposes two modes.
Apply supported fixes:
./reposhield fix ./demo-target
Preview them first:
./reposhield fix ./demo-target --dry-run
The second mode is important.
Automatic source modification is powerful, but it should not mean:
"Run the tool and hope."
Dry-run gives the developer a safety boundary:
Analyze → Identify fix → Preview → Developer decides → Apply
For the current implementation, automatic remediation is intentionally limited to supported rules rather than pretending every security issue can safely be fixed automatically.
That limitation is deliberate.
Why JSON wasn't enough
Terminal output is useful for a human.
CI systems don't want to scrape terminal text.
Other tools don't want to parse:
Risk Level: CRITICAL
from a console.
So RepoShield supports machine-readable reports.
JSON:
./reposhield analyze ./my-project --json report.json
SARIF:
./reposhield analyze ./my-project --sarif report.sarif
The SARIF output is especially useful because security tooling needs a standardized way to communicate findings to other developer and code-scanning systems.
The important part of the implementation was not building a general-purpose serialization framework.
It was implementing the report structures RepoShield actually needs.
Git intelligence without turning Git into a hard dependency
Repository analysis also becomes more useful when the tool understands the repository's Git state.
RepoShield has a separate:
./reposhield git ./my-project
command.
It can report:
- Current branch
- Repository status
- Clean / dirty state
- Tracked files
- Modified files
- Staged files
- Untracked files
- Commit count
- Latest commit information
Git is therefore an intelligence layer rather than a requirement for the core repository scanner.
A non-Git directory can still be analyzed.
If Git information exists, RepoShield can use it.
That separation was important because I didn't want Git functionality to define the entire application architecture.
The feature that turns a scanner into a CI gate
This was one of the most important pieces.
Suppose a repository contains a critical security issue.
Printing:
Risk Level: CRITICAL
doesn't automatically stop anything.
A CI system needs a machine-readable result.
RepoShield therefore uses meaningful exit codes:
0 → analysis completed successfully and policy passed
1 → analysis error or configured security policy violation
The resulting workflow is:
Repository
↓
RepoShield analyze
↓
Security Analysis
↓
Policy Evaluation
↓
PASS → Exit 0 → Continue
or
FAIL → Exit 1 → Stop
This changes RepoShield from:
"a command that prints security findings"
into:
"a command that can participate in an automated security gate."
That is a much more useful property for a developer tool.
Configuration makes the result enforceable
The policy layer allows the project to decide which findings should cause the command to fail.
For example:
security: fail_on: [CRITICAL, HIGH]
That means security analysis and policy enforcement remain separate concepts.
A finding can exist.
The risk engine can classify it.
The policy engine can then decide whether it should block the workflow.
The vulnerable demonstration repository therefore fails the configured policy:
Security Policy: FAILED
Exit Code: 1
A clean repository can pass:
Security Policy: PASSED
Exit Code: 0
This makes the analyzer useful in automated workflows rather than only during local inspection.
The CLI became the integration point
Instead of exposing a collection of unrelated executables, RepoShield uses one interface:
./reposhield analyze <path>
./reposhield fix <path>
./reposhield git <path>
With options such as:
--json <file>--sarif <file>--dry-run--help
The help system is also command-aware:
./reposhield --help
./reposhield analyze --help
./reposhield fix --help
./reposhield git --help
This sounds like a small detail.
It isn't.
Developer tools live or die by how quickly someone can understand their interface.
A technically powerful scanner with a confusing CLI is still a painful tool.
What the final architecture looks like
The implementation is split into focused components:
src/
├── core/
│ ├── FileScanner
│ └── RepositoryStats
│
├── codelens/
│ └── CodeLens
│
├── security/
│ └── SecurityAnalyzer
│
├── supplychain/
│ └── DependencyAnalyzer
│
├── graph/
│ └── DependencyGraph
│
├── risk/
│ └── RiskScorer
│
├── remediation/
│ └── RemediationEngine
│
├── reporting/
│ ├── HealthReport
│ ├── JsonReport
│ └── SarifReport
│
├── git/
│ └── GitAnalyzer
│
└── config/
└── Config
The architecture is intentionally modular.
The file scanner doesn't need to know how security rules work.
The security analyzer doesn't need to know how SARIF is written.
The risk scorer doesn't need to know how files were discovered.
The Git analyzer doesn't need to be involved in source scanning.
That separation made it possible to keep adding capabilities without turning main.cpp into the entire application.
What was actually difficult
The most difficult part wasn't writing individual detection rules.
A rule such as:
Find system()
is relatively straightforward.
The harder problem is everything around it.
A useful security finding needs:
- Reliable location
- Correct rule ID
- Severity
- Description
- Remediation
- Risk contribution
- Report representation
- Policy behaviour
And those representations have to remain consistent.
For example:
SecurityAnalyzer
↓
SecurityIssue
├── Risk
├── Remediation
└── Reports
↓
Policy
↓
Exit status
That's where a collection of features becomes a system.
Why pattern-based analysis needs honesty
One of the biggest lessons was that detecting a pattern isn't the same as proving a vulnerability.
For example:
std::system(...)
is security-sensitive.
But its presence alone doesn't prove exploitation.
The same applies to:
std::remove(...)
or a string that looks like an API key.
That's why RepoShield's findings use language such as:
- Possible hardcoded secret
- Potential SQL injection
- Potentially dangerous file operation
rather than pretending a lightweight pattern-based analyzer has the same certainty as a full semantic analysis engine.
Useful warnings are better than false certainty.
Why I didn't try to make it an AI security scanner
AI is everywhere in hackathon projects.
That made one design decision relatively easy:
I didn't want RepoShield's core value proposition to be "ask an AI whether your repository is secure."
Security analysis needs deterministic behaviour that a developer can inspect.
A rule such as:
RS003
Possible hardcoded secret
File: vulnerable.cpp
Line: 42
can be reproduced.
The risk calculation can be inspected.
The exit code can be tested.
The SARIF output can be consumed by another tool.
The remediation rule can be reviewed.
AI can help during development, but the final security decision shouldn't depend on a model improvising an answer every time the repository changes.
That was the direction I wanted for RepoShield:
deterministic analysis first, automation around it second.
What the zero-dependency constraint taught me
The biggest lesson wasn't:
"Third-party libraries are bad."
They aren't.
Libraries exist because rebuilding mature functionality can be wasteful.
The lesson was:
Understand the abstraction before deciding you need the abstraction.
C++17 already gives a lot:
std::filesystemstd::stringstd::vectorstd::mapstd::fstreamstd::algorithmstd::iostream
Those primitives are enough to build surprisingly capable developer tooling.
The difficult part is designing the system around them.
What I would normally install
If I weren't working under the zero-dependency constraint, I would likely reach for established libraries and frameworks for several pieces of this project.
For example:
- JSON libraries for structured serialization
- YAML libraries for configuration
- CLI parsing libraries
- C++ parsing or AST libraries
- Existing security-analysis frameworks
But replacing those conveniences forced me to implement the integration myself.
That meant dealing with:
- Parsing
- Escaping
- File traversal
- Structured output
- Configuration
- Command-line arguments
- Security rules
- Error handling
- CI integration
The result isn't that standard-library-only is always better.
The result is that I now understand much more clearly what those dependencies were actually doing for me.
What I would improve next
RepoShield is intentionally not pretending to be a replacement for every mature security platform.
There are obvious areas for future work.
More precise language analysis
The current Code Lens and security analysis are intentionally lightweight.
A full AST-based analysis engine would provide much deeper semantic understanding.
More security rules
The current rules cover several useful categories, but a production security analyzer would need a substantially broader rule set.
Better dependency resolution
Source-level includes are useful, but package manifests and resolved dependency versions would provide a deeper software supply-chain picture.
More remediation rules
Automatic fixes should remain conservative.
The right direction is to add more fixes only where the transformation can be made predictable and reviewable.
More testing
The next stage would be broader regression and adversarial testing across every analyzer and report format.
These aren't hidden gaps.
They're the next engineering milestones.
The thing I would not change
I would keep the separation between:
Detection
Risk
Remediation
Reporting
Policy
because that separation is what lets RepoShield behave like a toolchain instead of one giant scanner function.
A security finding can be:
detected
without automatically being:
blocking
A recommendation can exist without automatically modifying the source.
A report can exist without being the same thing as terminal output.
Those distinctions seem small.
They are what make the system composable.
The final result
RepoShield started with a fairly simple question:
Can a standalone C++17 program provide useful repository security intelligence without depending on a third-party C++ runtime ecosystem?
The answer became:
Yes.
But the more interesting result was everything around that answer.
RepoShield can now:
- scan repository files
- calculate repository statistics
- identify code structure
- detect 7 security issues
- classify severity
- calculate repository risk
- analyze dependencies
- build dependency graphs
- generate remediation guidance
- apply supported automatic fixes
- preview fixes with dry-run mode
- inspect Git repositories
- generate JSON reports
- generate SARIF reports
- enforce configurable security policies
- return meaningful CI exit codes
All through one CLI.
Discover
↓
Understand
↓
Detect
↓
Score
↓
Remediate
↓
Report
↓
Enforce
That's the part I care about most.
I didn't want to build another command that says:
"You have 7 security issues."
I wanted to build something that could answer:
"Here is what I found, here is why it matters, here is where it is, here is what you can do about it, here is the machine-readable result, and here is whether your security policy allows this repository to continue."
One final lesson
The zero-dependency constraint initially looked like a restriction.
It turned out to be an architectural forcing function.
Every time I wanted to reach for a library, I had to define the actual problem first.
Sometimes the answer would have been:
"Yes, use a library."
But sometimes it was:
"We only need 5% of what that library provides."
And that second answer is where RepoShield became interesting.
The real challenge wasn't removing dependencies.
It was owning the pieces of the security workflow that actually mattered.
And that is what I ended up building.
Try RepoShield
Repository: https://github.com/Danish84295/reposhield-hackathon
Demo: https://www.youtube.com/watch?v=Y0PwYIm7sYM&t=6s
Example:
./reposhield analyze ./my-project
Generate JSON:
./reposhield analyze ./my-project --json report.json
Generate SARIF:
./reposhield analyze ./my-project --sarif report.sarif
Preview remediation:
./reposhield fix ./my-project --dry-run
Inspect Git intelligence:
./reposhield git ./my-project
Built with
- C++17
- Standard C++ library
- Filesystem analysis
- Static security rules
- Risk scoring
- Dependency analysis
- Git intelligence
- JSON reporting
- SARIF reporting
- Configurable security policies
No third-party C++ runtime dependency is required by RepoShield's core implementation.
If you're building developer tooling, I'd love to hear what you would make if "just install a package" wasn't an option.
Top comments (0)