Skip to content

Commit e3e758a

Browse files
authored
Merge pull request #288 from KarenZita01/feature/security-testing-suite
Implement comprehensive security testing suite
2 parents 0e6c57a + dfa6e08 commit e3e758a

21 files changed

Lines changed: 6410 additions & 8238 deletions

.github/workflows/comprehensive-tests.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,11 @@ jobs:
112112
uses: actions/setup-node@v4
113113
with:
114114
node-version: '20'
115-
cache: 'npm'
116-
cache-dependency-path: frontend/package-lock.json
117115

118116
- name: Install frontend dependencies
119117
run: |
120118
cd frontend
121-
npm ci
119+
npm install
122120
123121
- name: TypeScript compilation check
124122
run: |
@@ -185,8 +183,11 @@ jobs:
185183
${{ runner.os }}-cargo-
186184
187185
- name: Install stellar-cli
188-
run: cargo install stellar-cli --locked
189-
186+
uses: stellar/stellar-cli@v27.0.0
187+
188+
- name: Install wasm32v1-none target
189+
run: rustup target add wasm32v1-none
190+
190191
- name: Build contract
191192
run: stellar contract build
192193
working-directory: ./contract
@@ -249,7 +250,6 @@ jobs:
249250
250251
- name: Build contract
251252
run: |
252-
cargo install stellar-cli --locked
253253
stellar contract build
254254
working-directory: ./contract
255255

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Security Tests
2+
3+
on:
4+
push:
5+
branches: [main, develop]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
security-tests:
11+
name: Run Security Tests
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Node.js 20
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: "20"
22+
cache: "npm"
23+
cache-dependency-path: security-tests/package-lock.json
24+
25+
- name: Install dependencies
26+
run: npm ci
27+
working-directory: ./security-tests
28+
29+
- name: TypeScript compilation check
30+
run: npx tsc --noEmit
31+
working-directory: ./security-tests
32+
33+
- name: Run OWASP security tests
34+
run: npm run test:owasp
35+
working-directory: ./security-tests
36+
37+
- name: Run penetration tests
38+
run: npm run test:penetration
39+
working-directory: ./security-tests
40+
41+
- name: Run dependency scanner
42+
run: npm run scan:dependencies
43+
working-directory: ./security-tests
44+
45+
- name: Run code security scanner
46+
run: npm run scan:code
47+
working-directory: ./security-tests
48+
49+
- name: Generate security report
50+
run: npm run report:security
51+
working-directory: ./security-tests
52+
if: always()
53+
54+
- name: Upload security report
55+
uses: actions/upload-artifact@v4
56+
if: always()
57+
with:
58+
name: security-report
59+
path: security-tests/reports/
60+
retention-days: 30

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
${{ runner.os }}-cargo-
3434
3535
- name: Install stellar-cli
36-
run: cargo install stellar-cli --locked
36+
uses: stellar/stellar-cli@v27.0.0
3737

3838
- name: Build contract
3939
run: stellar contract build

contract/src/lib.rs

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#![no_std]
2-
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, token, Symbol, Vec, Map, i64};
2+
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, token, Symbol, Vec, Map};
33

44
// Refund Status Enum
55
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -164,7 +164,7 @@ impl NepaBillingContract {
164164
panic!("Only admin can set refund approvers");
165165
}
166166

167-
if threshold == 0 || threshold as usize > approvers.len() {
167+
if threshold == 0 || threshold > approvers.len() {
168168
panic!("Invalid approval threshold");
169169
}
170170

@@ -187,14 +187,14 @@ impl NepaBillingContract {
187187
if meter_id_len < METER_ID_MIN_LENGTH || meter_id_len > METER_ID_MAX_LENGTH {
188188
panic!("Meter ID must be between 3 and 50 characters");
189189
}
190-
let meter_id_bytes = meter_id.as_bytes();
191-
for i in 0..meter_id_len as usize {
192-
let b = meter_id_bytes[i];
193-
let is_valid = (b >= b'A' && b <= b'Z')
194-
|| (b >= b'a' && b <= b'z')
195-
|| (b >= b'0' && b <= b'9')
196-
|| b == b'-'
197-
|| b == b'_';
190+
let meter_id_bytes: Vec<u8> = meter_id.clone().into();
191+
for i in 0..meter_id_len {
192+
let b = meter_id_bytes.get(i as u32).unwrap_or(0);
193+
let is_valid = (b >= 65 && b <= 90)
194+
|| (b >= 97 && b <= 122)
195+
|| (b >= 48 && b <= 57)
196+
|| b == 45
197+
|| b == 95;
198198
if !is_valid {
199199
panic!("Meter ID contains invalid characters (alphanumeric, hyphens, and underscores only)");
200200
}
@@ -288,7 +288,7 @@ impl NepaBillingContract {
288288
reviewer: reviewer.clone(),
289289
rating,
290290
comment,
291-
timestamp: env.ledger().timestamp(),
291+
timestamp: env.ledger().timestamp() as i64,
292292
transaction_hash,
293293
};
294294

@@ -324,7 +324,7 @@ impl NepaBillingContract {
324324
env.storage().persistent().get(&stats_key).unwrap_or(RatingStats {
325325
total_reviews: 0,
326326
average_rating: 0,
327-
rating_counts: Vec::from_array(&env, &[0, 0, 0, 0, 0]),
327+
rating_counts: Vec::from_array(&env, [0i64, 0, 0, 0, 0]),
328328
})
329329
}
330330

@@ -341,7 +341,7 @@ impl NepaBillingContract {
341341
let mut stats: RatingStats = env.storage().persistent().get(&stats_key).unwrap_or(RatingStats {
342342
total_reviews: 0,
343343
average_rating: 0,
344-
rating_counts: Vec::from_array(&env, &[0, 0, 0, 0, 0]),
344+
rating_counts: Vec::from_array(&env, [0i64, 0, 0, 0, 0]),
345345
});
346346

347347
// Update total reviews
@@ -381,12 +381,12 @@ impl NepaBillingContract {
381381
.unwrap_or_else(|| panic!("Refund config not found"));
382382

383383
if !config.enabled || config.paused {
384-
panic!(REFUNDS_PAUSED);
384+
panic!("{}", REFUNDS_PAUSED);
385385
}
386386

387387
// Get payment record
388388
let payment_record: PaymentRecord = env.storage().persistent().get(&payment_id)
389-
.unwrap_or_else(|| panic!(PAYMENT_NOT_FOUND));
389+
.unwrap_or_else(|| panic!("{}", PAYMENT_NOT_FOUND));
390390

391391
// Validation: Only original payer can request refund
392392
if payment_record.payer != requester {
@@ -401,17 +401,17 @@ impl NepaBillingContract {
401401
// Validation: Check refund window (24 hours)
402402
let current_time = env.ledger().timestamp();
403403
if current_time > payment_record.timestamp + config.refund_window_seconds {
404-
panic!(REFUND_WINDOW_EXPIRED);
404+
panic!("{}", REFUND_WINDOW_EXPIRED);
405405
}
406406

407407
// Validation: Check amount limits
408408
if payment_record.amount > config.max_refund_amount {
409-
panic!(INVALID_REFUND_AMOUNT);
409+
panic!("{}", INVALID_REFUND_AMOUNT);
410410
}
411411

412412
// Validation: Check reason length
413413
if reason.len() == 0 || reason.len() > 500 {
414-
panic!(INVALID_REFUND_REASON);
414+
panic!("{}", INVALID_REFUND_REASON);
415415
}
416416

417417
// Generate refund ID
@@ -464,25 +464,25 @@ impl NepaBillingContract {
464464
.unwrap_or_else(|| panic!("Refund config not found"));
465465

466466
if !config.approvers.contains(&approver) {
467-
panic!(INVALID_APPROVER);
467+
panic!("{}", INVALID_APPROVER);
468468
}
469469

470470
// Get refund request
471471
let refund_key = (REFUND_REQUESTS, refund_id);
472472
let mut refund_request: RefundRequest = env.storage().persistent().get(&refund_key)
473-
.unwrap_or_else(|| panic!(REFUND_NOT_FOUND));
473+
.unwrap_or_else(|| panic!("{}", REFUND_NOT_FOUND));
474474

475475
// Check if refund is still pending
476476
if refund_request.status != RefundStatus::Pending {
477-
panic!(REFUND_ALREADY_PROCESSED);
477+
panic!("{}", REFUND_ALREADY_PROCESSED);
478478
}
479479

480480
// Check expiration
481481
let current_time = env.ledger().timestamp();
482482
if current_time > refund_request.expiration {
483483
refund_request.status = RefundStatus::Expired;
484484
env.storage().persistent().set(&refund_key, &refund_request);
485-
panic!(REFUND_EXPIRED);
485+
panic!("{}", REFUND_EXPIRED);
486486
}
487487

488488
// Check if approver already approved
@@ -523,17 +523,17 @@ impl NepaBillingContract {
523523
.unwrap_or_else(|| panic!("Refund config not found"));
524524

525525
if !config.approvers.contains(&approver) {
526-
panic!(INVALID_APPROVER);
526+
panic!("{}", INVALID_APPROVER);
527527
}
528528

529529
// Get refund request
530530
let refund_key = (REFUND_REQUESTS, refund_id);
531531
let mut refund_request: RefundRequest = env.storage().persistent().get(&refund_key)
532-
.unwrap_or_else(|| panic!(REFUND_NOT_FOUND));
532+
.unwrap_or_else(|| panic!("{}", REFUND_NOT_FOUND));
533533

534534
// Check if refund is still pending
535535
if refund_request.status != RefundStatus::Pending {
536-
panic!(REFUND_ALREADY_PROCESSED);
536+
panic!("{}", REFUND_ALREADY_PROCESSED);
537537
}
538538

539539
// Mark as rejected
@@ -560,7 +560,7 @@ impl NepaBillingContract {
560560
// Get refund request
561561
let refund_key = (REFUND_REQUESTS, refund_id);
562562
let refund_request: RefundRequest = env.storage().persistent().get(&refund_key)
563-
.unwrap_or_else(|| panic!(REFUND_NOT_FOUND));
563+
.unwrap_or_else(|| panic!("{}", REFUND_NOT_FOUND));
564564

565565
// Check if refund is approved
566566
if refund_request.status != RefundStatus::Approved {
@@ -569,7 +569,7 @@ impl NepaBillingContract {
569569

570570
// Get payment record
571571
let mut payment_record: PaymentRecord = env.storage().persistent().get(&refund_request.original_payment_id)
572-
.unwrap_or_else(|| panic!(PAYMENT_NOT_FOUND));
572+
.unwrap_or_else(|| panic!("{}", PAYMENT_NOT_FOUND));
573573

574574
// Check if payment is already refunded
575575
if payment_record.is_refunded {
@@ -682,8 +682,8 @@ impl NepaBillingContract {
682682
if is_add {
683683
// Add approver if not already present
684684
if !config.approvers.contains(&approver) {
685-
config.approvers.push_back(approver);
686-
env.storage().persistent().set((APPROVER_STATUS, approver), &true);
685+
config.approvers.push_back(approver.clone());
686+
env.storage().persistent().set(&(APPROVER_STATUS, approver), &true);
687687
}
688688
} else {
689689
// Remove approver

0 commit comments

Comments
 (0)