DEV Community

DANISH AHMAD
DANISH AHMAD

Posted on

A Security Scanner Is Easy. Building One You Can Actually Trust Is Not.

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::filesystem
  • std::string
  • std::vector
  • std::map
  • std::fstream
  • std::algorithm
  • std::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)