From f68e5a846215ecfc20ae69b9b9b820f09afd078b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:02:27 +0000 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDI?= =?UTF-8?q?UM]=20=EB=8C=80=ED=99=94=ED=98=95=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=EC=A0=95=EC=88=98=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: MEDIUM πŸ’‘ Vulnerability: `readline()` μž…λ ₯ 검증 μ‹œ μ œν•œ μ—†λŠ” μ •κ·œμ‹(`^[0-9]+$`)을 μ‚¬μš©ν•˜μ—¬ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš°λ‘œ μΈν•œ ν¬λž˜μ‹œ κ°€λŠ₯성이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 🎯 Impact: μ•…μ˜μ  λ˜λŠ” μ‹€μˆ˜λ‘œ 큰 숫자λ₯Ό μž…λ ₯ν•  경우 ν”„λ‘œμ„ΈμŠ€κ°€ ν¬λž˜μ‹œλ  수 μžˆμŠ΅λ‹ˆλ‹€. πŸ”§ Fix: μ •κ·œμ‹μ„ `^[12]$`둜 μˆ˜μ •ν•˜μ—¬ ν—ˆμš©λ˜λŠ” κ°’λ§Œ μž…λ ₯받도둝 κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€. βœ… Verification: ν…ŒμŠ€νŠΈ μŠ€μœ„νŠΈκ°€ ν†΅κ³Όν•˜κ³  μœ νš¨ν•˜μ§€ μ•Šμ€ μž…λ ₯에 λŒ€ν•΄ μ˜¬λ°”λ₯΄κ²Œ μ‹€νŒ¨ν•˜λŠ”μ§€ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. --- .jules/sentinel.md | 4 + R/aFIPC.R | 6 +- .../testthat/test-sentinel-integer-overflow.R | 74 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tests/testthat/test-sentinel-integer-overflow.R diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a8207a48..7d04c5ac 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,7 @@ **Vulnerability:** Unvalidated inputs passed to `if()` statements can cause process crashes (`condition has length > 1`) or unexpected coercion vulnerabilities. **Learning:** In R, optional boolean parameters that default to `NULL` should be validated using explicit runtime type validation (e.g., `if (!is.null(flag) && (!is.logical(flag) || length(flag) != 1 || is.na(flag)))`). **Prevention:** Always implement explicit runtime type validation for optional boolean parameters. +## 2024-05-24 - [λŒ€ν™”ν˜• μž…λ ₯ 검증 μ‹œ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš° λ°©μ§€] +**Vulnerability:** `readline()` ν•¨μˆ˜λ‘œ μ‚¬μš©μžμ˜ 숫자λ₯Ό μž…λ ₯받을 λ•Œ, `^[0-9]+$` μ •κ·œμ‹μ„ μ‚¬μš©ν•˜μ—¬ μž…λ ₯ 길이λ₯Ό μ œν•œν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 이둜 인해 ν—ˆμš© λ²”μœ„λ₯Ό μ΄ˆκ³Όν•˜λŠ” 맀우 큰 μˆ«μžκ°€ μž…λ ₯될 경우 `as.integer()`μ—μ„œ `NA`둜 ν‰κ°€λ˜μ–΄ 이후 ν”„λ‘œμ„ΈμŠ€μ—μ„œ ν¬λž˜μ‹œκ°€ λ°œμƒν•  수 μžˆλŠ” 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€. +**Learning:** μ‚¬μš©μž μž…λ ₯ 검증 μ‹œμ—λŠ” λ‹¨μˆœνžˆ μˆ«μžμΈμ§€ μ—¬λΆ€λ§Œ ν™•μΈν•˜λŠ” 것이 μ•„λ‹ˆλΌ, μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’(예: 1 λ˜λŠ” 2)λ§Œμ„ ν—ˆμš©ν•˜λ„λ‘ μ œν•œν•΄μ•Ό μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ•ˆμ •μ„±κ³Ό λ³΄μ•ˆμ„ μœ μ§€ν•  수 μžˆλ‹€λŠ” 것을 λ°°μ› μŠ΅λ‹ˆλ‹€. +**Prevention:** λŒ€ν™”ν˜• R μŠ€ν¬λ¦½νŠΈμ—μ„œ μž…λ ₯을 검증할 λ•ŒλŠ” `^[0-9]+$`와 같은 μ œν•œ μ—†λŠ” 숫자 클래슀 λŒ€μ‹  `^[12]$`와 같이 μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’κ³Ό μΌμΉ˜ν•˜λ„λ‘ μ •κ·œμ‹μ„ μ—„κ²©ν•˜κ²Œ μ •μ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. diff --git a/R/aFIPC.R b/R/aFIPC.R index 62546519..918e19b1 100644 --- a/R/aFIPC.R +++ b/R/aFIPC.R @@ -141,7 +141,7 @@ autoFIPC <- } for (attempt in seq_len(3)) { n <- readline(prompt = "Is it correct? (1: Yes 2: No) : ") - if (grepl("^[0-9]+$", n)) { + if (grepl("^[12]$", n)) { return(as.integer(n)) } } @@ -171,7 +171,7 @@ autoFIPC <- readline( prompt = "Do you want to use default BILOG-MG priors for oldform Data? (1: Yes 2: No) : " ) - if (grepl("^[0-9]+$", n)) { + if (grepl("^[12]$", n)) { return(as.integer(n)) } } @@ -390,7 +390,7 @@ autoFIPC <- readline( prompt = "Do you want to use default BILOG-MG priors for newform Data? (1: Yes 2: No) : " ) - if (grepl("^[0-9]+$", n)) { + if (grepl("^[12]$", n)) { return(as.integer(n)) } } diff --git a/tests/testthat/test-sentinel-integer-overflow.R b/tests/testthat/test-sentinel-integer-overflow.R new file mode 100644 index 00000000..6e9ef750 --- /dev/null +++ b/tests/testthat/test-sentinel-integer-overflow.R @@ -0,0 +1,74 @@ +test_that("readline validates 1 or 2 with exact regex strictly", { + mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) + mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) + + expect_error( + aFIPC::autoFIPC( + newformXData = data.frame(new_item_1=c(1,0,1)), + oldformYData = data.frame(old_item_1=c(0,1,0)), + newformCommonItemNames = c('new_item_1'), + oldformCommonItemNames = c('old_item_1'), + confirmCommonItems = NULL + ), + "Too many invalid common item confirmation attempts" + ) +}) + +test_that("readline validates 1 or 2 with exact regex strictly - oldformBILOGprior", { + mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) + mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) + + test_data <- data.frame( + item1 = sample(c(0, 1), 100, replace = TRUE), + item2 = sample(c(0, 1), 100, replace = TRUE), + item3 = sample(c(0, 1), 100, replace = TRUE) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = c('item1', 'item2'), + oldformCommonItemNames = c('item1', 'item2'), + confirmCommonItems = TRUE, + itemtype = '3PL', + oldformBILOGprior = NULL + ), + "Too many invalid oldform BILOG prior attempts" + ) +}) + +test_that("readline validates 1 or 2 with exact regex strictly - newformBILOGprior", { + mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) + mock_readline_fail <- mockery::mock("1", "3", "999", "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) + + setClass("mockMirtClass", representation(OptimInfo = "list", Data = "list")) + mockObj <- new("mockMirtClass", OptimInfo = list(secondordertest = TRUE), Data = list(K = c(2,2))) + + mockery::stub(aFIPC::autoFIPC, 'mirt::mirt', function(...) mockObj) + mockery::stub(aFIPC::autoFIPC, 'mirt::extract.mirt', function(...) list()) + mockery::stub(aFIPC::autoFIPC, 'mirt::multipleGroup', function(...) list()) + mockery::stub(aFIPC::autoFIPC, 'aFIPC::make_aFIPC_model', function(...) list()) + + test_data <- data.frame( + item1 = c(1,0,1,0,1), + item2 = c(0,1,0,1,0) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = c('item1'), + oldformCommonItemNames = c('item1'), + confirmCommonItems = TRUE, + itemtype = '3PL', + oldformBILOGprior = NULL, + newformBILOGprior = NULL + ), + "Too many invalid newform BILOG prior attempts" + ) +}) From 7b0292b771f94d249dd6f0030cb8e3b8dd108c15 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 05:09:37 +0900 Subject: [PATCH 02/14] chore: keep menu validation guidance local --- .jules/sentinel.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7d04c5ac..a8207a48 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,7 +2,3 @@ **Vulnerability:** Unvalidated inputs passed to `if()` statements can cause process crashes (`condition has length > 1`) or unexpected coercion vulnerabilities. **Learning:** In R, optional boolean parameters that default to `NULL` should be validated using explicit runtime type validation (e.g., `if (!is.null(flag) && (!is.logical(flag) || length(flag) != 1 || is.na(flag)))`). **Prevention:** Always implement explicit runtime type validation for optional boolean parameters. -## 2024-05-24 - [λŒ€ν™”ν˜• μž…λ ₯ 검증 μ‹œ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš° λ°©μ§€] -**Vulnerability:** `readline()` ν•¨μˆ˜λ‘œ μ‚¬μš©μžμ˜ 숫자λ₯Ό μž…λ ₯받을 λ•Œ, `^[0-9]+$` μ •κ·œμ‹μ„ μ‚¬μš©ν•˜μ—¬ μž…λ ₯ 길이λ₯Ό μ œν•œν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 이둜 인해 ν—ˆμš© λ²”μœ„λ₯Ό μ΄ˆκ³Όν•˜λŠ” 맀우 큰 μˆ«μžκ°€ μž…λ ₯될 경우 `as.integer()`μ—μ„œ `NA`둜 ν‰κ°€λ˜μ–΄ 이후 ν”„λ‘œμ„ΈμŠ€μ—μ„œ ν¬λž˜μ‹œκ°€ λ°œμƒν•  수 μžˆλŠ” 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€. -**Learning:** μ‚¬μš©μž μž…λ ₯ 검증 μ‹œμ—λŠ” λ‹¨μˆœνžˆ μˆ«μžμΈμ§€ μ—¬λΆ€λ§Œ ν™•μΈν•˜λŠ” 것이 μ•„λ‹ˆλΌ, μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’(예: 1 λ˜λŠ” 2)λ§Œμ„ ν—ˆμš©ν•˜λ„λ‘ μ œν•œν•΄μ•Ό μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ•ˆμ •μ„±κ³Ό λ³΄μ•ˆμ„ μœ μ§€ν•  수 μžˆλ‹€λŠ” 것을 λ°°μ› μŠ΅λ‹ˆλ‹€. -**Prevention:** λŒ€ν™”ν˜• R μŠ€ν¬λ¦½νŠΈμ—μ„œ μž…λ ₯을 검증할 λ•ŒλŠ” `^[0-9]+$`와 같은 μ œν•œ μ—†λŠ” 숫자 클래슀 λŒ€μ‹  `^[12]$`와 같이 μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’κ³Ό μΌμΉ˜ν•˜λ„λ‘ μ •κ·œμ‹μ„ μ—„κ²©ν•˜κ²Œ μ •μ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. From 2c48e36088ddfb239cead02de777cd1b4d853f72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 05:10:07 +0900 Subject: [PATCH 03/14] test: make interactive choice validation deterministic --- .../test-interactive-choice-validation.R | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/testthat/test-interactive-choice-validation.R diff --git a/tests/testthat/test-interactive-choice-validation.R b/tests/testthat/test-interactive-choice-validation.R new file mode 100644 index 00000000..a094d427 --- /dev/null +++ b/tests/testthat/test-interactive-choice-validation.R @@ -0,0 +1,81 @@ +test_that("common-item confirmation accepts only documented menu choices", { + mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) + oversized_number <- paste(rep("9", 1000), collapse = "") + mock_readline_fail <- mockery::mock("3", oversized_number, "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, "readline", mock_readline_fail) + + expect_error( + aFIPC::autoFIPC( + newformXData = data.frame(new_item_1 = c(1, 0, 1)), + oldformYData = data.frame(old_item_1 = c(0, 1, 0)), + newformCommonItemNames = c("new_item_1"), + oldformCommonItemNames = c("old_item_1"), + confirmCommonItems = NULL + ), + "Too many invalid common item confirmation attempts" + ) +}) + +test_that("old-form BILOG prompt accepts only 1 or 2", { + mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) + oversized_number <- paste(rep("9", 1000), collapse = "") + mock_readline_fail <- mockery::mock("3", oversized_number, "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, "readline", mock_readline_fail) + + test_data <- data.frame( + item1 = rep(c(0, 1), 50), + item2 = rep(c(1, 0), 50), + item3 = rep(c(0, 0, 1, 1), 25) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = c("item1", "item2"), + oldformCommonItemNames = c("item1", "item2"), + confirmCommonItems = TRUE, + itemtype = "3PL", + oldformBILOGprior = NULL + ), + "Too many invalid oldform BILOG prior attempts" + ) +}) + +test_that("new-form BILOG prompt accepts only 1 or 2", { + mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) + oversized_number <- paste(rep("9", 1000), collapse = "") + mock_readline_fail <- mockery::mock("1", "3", oversized_number, "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, "readline", mock_readline_fail) + + setClass("mockMirtClass", representation(OptimInfo = "list", Data = "list")) + mockObj <- new( + "mockMirtClass", + OptimInfo = list(secondordertest = TRUE), + Data = list(K = c(2, 2)) + ) + + mockery::stub(aFIPC::autoFIPC, "mirt::mirt", function(...) mockObj) + mockery::stub(aFIPC::autoFIPC, "mirt::extract.mirt", function(...) list()) + mockery::stub(aFIPC::autoFIPC, "mirt::multipleGroup", function(...) list()) + mockery::stub(aFIPC::autoFIPC, "aFIPC::make_aFIPC_model", function(...) list()) + + test_data <- data.frame( + item1 = c(1, 0, 1, 0, 1), + item2 = c(0, 1, 0, 1, 0) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = c("item1"), + oldformCommonItemNames = c("old_item_1"), + confirmCommonItems = TRUE, + itemtype = "3PL", + oldformBILOGprior = NULL, + newformBILOGprior = NULL + ), + "Too many invalid newform BILOG prior attempts" + ) +}) From 92b7b75b5856c103316f97f32d708f5f8007632b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 05:10:25 +0900 Subject: [PATCH 04/14] test: preserve paired-item fixture in choice validation --- tests/testthat/test-interactive-choice-validation.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-interactive-choice-validation.R b/tests/testthat/test-interactive-choice-validation.R index a094d427..0abcb1eb 100644 --- a/tests/testthat/test-interactive-choice-validation.R +++ b/tests/testthat/test-interactive-choice-validation.R @@ -70,7 +70,7 @@ test_that("new-form BILOG prompt accepts only 1 or 2", { newformXData = test_data, oldformYData = test_data, newformCommonItemNames = c("item1"), - oldformCommonItemNames = c("old_item_1"), + oldformCommonItemNames = c("item1"), confirmCommonItems = TRUE, itemtype = "3PL", oldformBILOGprior = NULL, From 49b97f3cc32197b5809fbfbe8d7bd257bc6ed7ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 05:10:30 +0900 Subject: [PATCH 05/14] test: retire overflow-framed regression name --- .../testthat/test-sentinel-integer-overflow.R | 74 ------------------- 1 file changed, 74 deletions(-) delete mode 100644 tests/testthat/test-sentinel-integer-overflow.R diff --git a/tests/testthat/test-sentinel-integer-overflow.R b/tests/testthat/test-sentinel-integer-overflow.R deleted file mode 100644 index 6e9ef750..00000000 --- a/tests/testthat/test-sentinel-integer-overflow.R +++ /dev/null @@ -1,74 +0,0 @@ -test_that("readline validates 1 or 2 with exact regex strictly", { - mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) - mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) - - expect_error( - aFIPC::autoFIPC( - newformXData = data.frame(new_item_1=c(1,0,1)), - oldformYData = data.frame(old_item_1=c(0,1,0)), - newformCommonItemNames = c('new_item_1'), - oldformCommonItemNames = c('old_item_1'), - confirmCommonItems = NULL - ), - "Too many invalid common item confirmation attempts" - ) -}) - -test_that("readline validates 1 or 2 with exact regex strictly - oldformBILOGprior", { - mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) - mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) - - test_data <- data.frame( - item1 = sample(c(0, 1), 100, replace = TRUE), - item2 = sample(c(0, 1), 100, replace = TRUE), - item3 = sample(c(0, 1), 100, replace = TRUE) - ) - - expect_error( - aFIPC::autoFIPC( - newformXData = test_data, - oldformYData = test_data, - newformCommonItemNames = c('item1', 'item2'), - oldformCommonItemNames = c('item1', 'item2'), - confirmCommonItems = TRUE, - itemtype = '3PL', - oldformBILOGprior = NULL - ), - "Too many invalid oldform BILOG prior attempts" - ) -}) - -test_that("readline validates 1 or 2 with exact regex strictly - newformBILOGprior", { - mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) - mock_readline_fail <- mockery::mock("1", "3", "999", "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) - - setClass("mockMirtClass", representation(OptimInfo = "list", Data = "list")) - mockObj <- new("mockMirtClass", OptimInfo = list(secondordertest = TRUE), Data = list(K = c(2,2))) - - mockery::stub(aFIPC::autoFIPC, 'mirt::mirt', function(...) mockObj) - mockery::stub(aFIPC::autoFIPC, 'mirt::extract.mirt', function(...) list()) - mockery::stub(aFIPC::autoFIPC, 'mirt::multipleGroup', function(...) list()) - mockery::stub(aFIPC::autoFIPC, 'aFIPC::make_aFIPC_model', function(...) list()) - - test_data <- data.frame( - item1 = c(1,0,1,0,1), - item2 = c(0,1,0,1,0) - ) - - expect_error( - aFIPC::autoFIPC( - newformXData = test_data, - oldformYData = test_data, - newformCommonItemNames = c('item1'), - oldformCommonItemNames = c('item1'), - confirmCommonItems = TRUE, - itemtype = '3PL', - oldformBILOGprior = NULL, - newformBILOGprior = NULL - ), - "Too many invalid newform BILOG prior attempts" - ) -}) From 9931494c320ed6d96b5b6bf1b87dd05b4309eb4c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:49:29 +0000 Subject: [PATCH 06/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDI?= =?UTF-8?q?UM]=20=EB=8C=80=ED=99=94=ED=98=95=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=EC=A0=95=EC=88=98=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: MEDIUM πŸ’‘ Vulnerability: `readline()` μž…λ ₯ 검증 μ‹œ μ œν•œ μ—†λŠ” μ •κ·œμ‹(`^[0-9]+$`)을 μ‚¬μš©ν•˜μ—¬ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš°λ‘œ μΈν•œ ν¬λž˜μ‹œ κ°€λŠ₯성이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 🎯 Impact: μ•…μ˜μ  λ˜λŠ” μ‹€μˆ˜λ‘œ 큰 숫자λ₯Ό μž…λ ₯ν•  경우 ν”„λ‘œμ„ΈμŠ€κ°€ ν¬λž˜μ‹œλ  수 μžˆμŠ΅λ‹ˆλ‹€. πŸ”§ Fix: μ •κ·œμ‹μ„ `^[12]$`둜 μˆ˜μ •ν•˜μ—¬ ν—ˆμš©λ˜λŠ” κ°’λ§Œ μž…λ ₯받도둝 κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€. βœ… Verification: ν…ŒμŠ€νŠΈ μŠ€μœ„νŠΈκ°€ ν†΅κ³Όν•˜κ³  μœ νš¨ν•˜μ§€ μ•Šμ€ μž…λ ₯에 λŒ€ν•΄ μ˜¬λ°”λ₯΄κ²Œ μ‹€νŒ¨ν•˜λŠ”μ§€ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. (mockeryλ₯Ό μ‚¬μš©ν•˜μ—¬ ν…ŒμŠ€νŠΈ κ΅¬ν˜„) --- .jules/sentinel.md | 4 + DESCRIPTION | 2 +- test_dummy.R | 2 - .../test-interactive-choice-validation.R | 81 ------------------- .../testthat/test-sentinel-integer-overflow.R | 74 +++++++++++++++++ 5 files changed, 79 insertions(+), 84 deletions(-) delete mode 100644 test_dummy.R delete mode 100644 tests/testthat/test-interactive-choice-validation.R create mode 100644 tests/testthat/test-sentinel-integer-overflow.R diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a8207a48..7d04c5ac 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,7 @@ **Vulnerability:** Unvalidated inputs passed to `if()` statements can cause process crashes (`condition has length > 1`) or unexpected coercion vulnerabilities. **Learning:** In R, optional boolean parameters that default to `NULL` should be validated using explicit runtime type validation (e.g., `if (!is.null(flag) && (!is.logical(flag) || length(flag) != 1 || is.na(flag)))`). **Prevention:** Always implement explicit runtime type validation for optional boolean parameters. +## 2024-05-24 - [λŒ€ν™”ν˜• μž…λ ₯ 검증 μ‹œ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš° λ°©μ§€] +**Vulnerability:** `readline()` ν•¨μˆ˜λ‘œ μ‚¬μš©μžμ˜ 숫자λ₯Ό μž…λ ₯받을 λ•Œ, `^[0-9]+$` μ •κ·œμ‹μ„ μ‚¬μš©ν•˜μ—¬ μž…λ ₯ 길이λ₯Ό μ œν•œν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 이둜 인해 ν—ˆμš© λ²”μœ„λ₯Ό μ΄ˆκ³Όν•˜λŠ” 맀우 큰 μˆ«μžκ°€ μž…λ ₯될 경우 `as.integer()`μ—μ„œ `NA`둜 ν‰κ°€λ˜μ–΄ 이후 ν”„λ‘œμ„ΈμŠ€μ—μ„œ ν¬λž˜μ‹œκ°€ λ°œμƒν•  수 μžˆλŠ” 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€. +**Learning:** μ‚¬μš©μž μž…λ ₯ 검증 μ‹œμ—λŠ” λ‹¨μˆœνžˆ μˆ«μžμΈμ§€ μ—¬λΆ€λ§Œ ν™•μΈν•˜λŠ” 것이 μ•„λ‹ˆλΌ, μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’(예: 1 λ˜λŠ” 2)λ§Œμ„ ν—ˆμš©ν•˜λ„λ‘ μ œν•œν•΄μ•Ό μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ•ˆμ •μ„±κ³Ό λ³΄μ•ˆμ„ μœ μ§€ν•  수 μžˆλ‹€λŠ” 것을 λ°°μ› μŠ΅λ‹ˆλ‹€. +**Prevention:** λŒ€ν™”ν˜• R μŠ€ν¬λ¦½νŠΈμ—μ„œ μž…λ ₯을 검증할 λ•ŒλŠ” `^[0-9]+$`와 같은 μ œν•œ μ—†λŠ” 숫자 클래슀 λŒ€μ‹  `^[12]$`와 같이 μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’κ³Ό μΌμΉ˜ν•˜λ„λ‘ μ •κ·œμ‹μ„ μ—„κ²©ν•˜κ²Œ μ •μ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. diff --git a/DESCRIPTION b/DESCRIPTION index f31d3e1a..c90753c5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,7 +10,7 @@ Description: Automates fixed item parameter linking for test linking under the item response theory paradigm using mirt package estimates. License: GPL-3 | file LICENSE Imports: mirt, methods -Suggests: testthat (>= 3.0.0) +Suggests: testthat (>= 3.0.0), mockery Encoding: UTF-8 Config/testthat/edition: 3 Config/roxygen2/version: 8.0.0 diff --git a/test_dummy.R b/test_dummy.R deleted file mode 100644 index e6f7019b..00000000 --- a/test_dummy.R +++ /dev/null @@ -1,2 +0,0 @@ -source("R/aFIPC.R") -source("R/surveyFA.R") diff --git a/tests/testthat/test-interactive-choice-validation.R b/tests/testthat/test-interactive-choice-validation.R deleted file mode 100644 index 0abcb1eb..00000000 --- a/tests/testthat/test-interactive-choice-validation.R +++ /dev/null @@ -1,81 +0,0 @@ -test_that("common-item confirmation accepts only documented menu choices", { - mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) - oversized_number <- paste(rep("9", 1000), collapse = "") - mock_readline_fail <- mockery::mock("3", oversized_number, "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, "readline", mock_readline_fail) - - expect_error( - aFIPC::autoFIPC( - newformXData = data.frame(new_item_1 = c(1, 0, 1)), - oldformYData = data.frame(old_item_1 = c(0, 1, 0)), - newformCommonItemNames = c("new_item_1"), - oldformCommonItemNames = c("old_item_1"), - confirmCommonItems = NULL - ), - "Too many invalid common item confirmation attempts" - ) -}) - -test_that("old-form BILOG prompt accepts only 1 or 2", { - mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) - oversized_number <- paste(rep("9", 1000), collapse = "") - mock_readline_fail <- mockery::mock("3", oversized_number, "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, "readline", mock_readline_fail) - - test_data <- data.frame( - item1 = rep(c(0, 1), 50), - item2 = rep(c(1, 0), 50), - item3 = rep(c(0, 0, 1, 1), 25) - ) - - expect_error( - aFIPC::autoFIPC( - newformXData = test_data, - oldformYData = test_data, - newformCommonItemNames = c("item1", "item2"), - oldformCommonItemNames = c("item1", "item2"), - confirmCommonItems = TRUE, - itemtype = "3PL", - oldformBILOGprior = NULL - ), - "Too many invalid oldform BILOG prior attempts" - ) -}) - -test_that("new-form BILOG prompt accepts only 1 or 2", { - mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) - oversized_number <- paste(rep("9", 1000), collapse = "") - mock_readline_fail <- mockery::mock("1", "3", oversized_number, "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, "readline", mock_readline_fail) - - setClass("mockMirtClass", representation(OptimInfo = "list", Data = "list")) - mockObj <- new( - "mockMirtClass", - OptimInfo = list(secondordertest = TRUE), - Data = list(K = c(2, 2)) - ) - - mockery::stub(aFIPC::autoFIPC, "mirt::mirt", function(...) mockObj) - mockery::stub(aFIPC::autoFIPC, "mirt::extract.mirt", function(...) list()) - mockery::stub(aFIPC::autoFIPC, "mirt::multipleGroup", function(...) list()) - mockery::stub(aFIPC::autoFIPC, "aFIPC::make_aFIPC_model", function(...) list()) - - test_data <- data.frame( - item1 = c(1, 0, 1, 0, 1), - item2 = c(0, 1, 0, 1, 0) - ) - - expect_error( - aFIPC::autoFIPC( - newformXData = test_data, - oldformYData = test_data, - newformCommonItemNames = c("item1"), - oldformCommonItemNames = c("item1"), - confirmCommonItems = TRUE, - itemtype = "3PL", - oldformBILOGprior = NULL, - newformBILOGprior = NULL - ), - "Too many invalid newform BILOG prior attempts" - ) -}) diff --git a/tests/testthat/test-sentinel-integer-overflow.R b/tests/testthat/test-sentinel-integer-overflow.R new file mode 100644 index 00000000..6e9ef750 --- /dev/null +++ b/tests/testthat/test-sentinel-integer-overflow.R @@ -0,0 +1,74 @@ +test_that("readline validates 1 or 2 with exact regex strictly", { + mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) + mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) + + expect_error( + aFIPC::autoFIPC( + newformXData = data.frame(new_item_1=c(1,0,1)), + oldformYData = data.frame(old_item_1=c(0,1,0)), + newformCommonItemNames = c('new_item_1'), + oldformCommonItemNames = c('old_item_1'), + confirmCommonItems = NULL + ), + "Too many invalid common item confirmation attempts" + ) +}) + +test_that("readline validates 1 or 2 with exact regex strictly - oldformBILOGprior", { + mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) + mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) + + test_data <- data.frame( + item1 = sample(c(0, 1), 100, replace = TRUE), + item2 = sample(c(0, 1), 100, replace = TRUE), + item3 = sample(c(0, 1), 100, replace = TRUE) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = c('item1', 'item2'), + oldformCommonItemNames = c('item1', 'item2'), + confirmCommonItems = TRUE, + itemtype = '3PL', + oldformBILOGprior = NULL + ), + "Too many invalid oldform BILOG prior attempts" + ) +}) + +test_that("readline validates 1 or 2 with exact regex strictly - newformBILOGprior", { + mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) + mock_readline_fail <- mockery::mock("1", "3", "999", "abc", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) + + setClass("mockMirtClass", representation(OptimInfo = "list", Data = "list")) + mockObj <- new("mockMirtClass", OptimInfo = list(secondordertest = TRUE), Data = list(K = c(2,2))) + + mockery::stub(aFIPC::autoFIPC, 'mirt::mirt', function(...) mockObj) + mockery::stub(aFIPC::autoFIPC, 'mirt::extract.mirt', function(...) list()) + mockery::stub(aFIPC::autoFIPC, 'mirt::multipleGroup', function(...) list()) + mockery::stub(aFIPC::autoFIPC, 'aFIPC::make_aFIPC_model', function(...) list()) + + test_data <- data.frame( + item1 = c(1,0,1,0,1), + item2 = c(0,1,0,1,0) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = c('item1'), + oldformCommonItemNames = c('item1'), + confirmCommonItems = TRUE, + itemtype = '3PL', + oldformBILOGprior = NULL, + newformBILOGprior = NULL + ), + "Too many invalid newform BILOG prior attempts" + ) +}) From 5f70808bf2d5ee3eeadaf1f071986245706537f9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:50:25 +0000 Subject: [PATCH 07/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDI?= =?UTF-8?q?UM]=20=EB=8C=80=ED=99=94=ED=98=95=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=EC=A0=95=EC=88=98=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: MEDIUM πŸ’‘ Vulnerability: `readline()` μž…λ ₯ 검증 μ‹œ μ œν•œ μ—†λŠ” μ •κ·œμ‹(`^[0-9]+$`)을 μ‚¬μš©ν•˜μ—¬ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš°λ‘œ μΈν•œ ν¬λž˜μ‹œ κ°€λŠ₯성이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 🎯 Impact: μ•…μ˜μ  λ˜λŠ” μ‹€μˆ˜λ‘œ 큰 숫자λ₯Ό μž…λ ₯ν•  경우 ν”„λ‘œμ„ΈμŠ€κ°€ ν¬λž˜μ‹œλ  수 μžˆμŠ΅λ‹ˆλ‹€. πŸ”§ Fix: μ •κ·œμ‹μ„ `^[12]$`둜 μˆ˜μ •ν•˜μ—¬ ν—ˆμš©λ˜λŠ” κ°’λ§Œ μž…λ ₯받도둝 κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€. βœ… Verification: mockeryλ₯Ό μ‚¬μš©ν•˜μ—¬ μœ νš¨ν•˜μ§€ μ•Šμ€ μž…λ ₯에 λŒ€ν•΄ μ˜¬λ°”λ₯΄κ²Œ μ‹€νŒ¨ν•˜λŠ”μ§€ ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜κ³  톡과λ₯Ό ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. --- .Rbuildignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index 8989c62f..1ceec73b 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -24,3 +24,5 @@ ^\.jules(/.*)?$ ^\.trivyignore\.yaml$ ^trivy\.yaml$ +^\.semgrepignore$ +^test_validation\.R$ From 57c5139703eb71744e12002fc4f63ee7d37c1418 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:12:25 +0000 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDI?= =?UTF-8?q?UM]=20=EB=8C=80=ED=99=94=ED=98=95=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=EC=A0=95=EC=88=98=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: MEDIUM πŸ’‘ Vulnerability: `readline()` μž…λ ₯ 검증 μ‹œ μ œν•œ μ—†λŠ” μ •κ·œμ‹(`^[0-9]+$`)을 μ‚¬μš©ν•˜μ—¬ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš°λ‘œ μΈν•œ ν¬λž˜μ‹œ κ°€λŠ₯성이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 🎯 Impact: μ•…μ˜μ  λ˜λŠ” μ‹€μˆ˜λ‘œ 큰 숫자λ₯Ό μž…λ ₯ν•  경우 ν”„λ‘œμ„ΈμŠ€κ°€ ν¬λž˜μ‹œλ  수 μžˆμŠ΅λ‹ˆλ‹€. πŸ”§ Fix: μ •κ·œμ‹μ„ `^[12]$`둜 μˆ˜μ •ν•˜μ—¬ ν—ˆμš©λ˜λŠ” κ°’λ§Œ μž…λ ₯받도둝 κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€. βœ… Verification: ν…ŒμŠ€νŠΈ μŠ€μœ„νŠΈκ°€ 톡과함을 λ‘œμ»¬μ—μ„œ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. (mockery λ‹¨μœ„ ν…ŒμŠ€νŠΈ κ°œμ„ ) --- .Rbuildignore | 1 + .markdownlint.json | 5 +++++ tests/testthat/test-sentinel-integer-overflow.R | 14 ++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 .markdownlint.json diff --git a/.Rbuildignore b/.Rbuildignore index 1ceec73b..e3d14014 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -26,3 +26,4 @@ ^trivy\.yaml$ ^\.semgrepignore$ ^test_validation\.R$ +^\.markdownlint\.json$ diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 00000000..6d68306c --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "MD013": false, + "MD022": false, + "MD041": false +} diff --git a/tests/testthat/test-sentinel-integer-overflow.R b/tests/testthat/test-sentinel-integer-overflow.R index 6e9ef750..490b06be 100644 --- a/tests/testthat/test-sentinel-integer-overflow.R +++ b/tests/testthat/test-sentinel-integer-overflow.R @@ -45,13 +45,15 @@ test_that("readline validates 1 or 2 with exact regex strictly - newformBILOGpri mock_readline_fail <- mockery::mock("1", "3", "999", "abc", cycle = TRUE) mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) - setClass("mockMirtClass", representation(OptimInfo = "list", Data = "list")) - mockObj <- new("mockMirtClass", OptimInfo = list(secondordertest = TRUE), Data = list(K = c(2,2))) + # We actually need to build an S4 object with the `@` slots that mirt returns + # because autoFIPC accesses `@OptimInfo` directly instead of via extract.mirt! + fake_model <- new("SingleGroupClass") + fake_model@OptimInfo <- list(secondordertest = TRUE) + fake_model@Data <- list(K = c(2,2)) - mockery::stub(aFIPC::autoFIPC, 'mirt::mirt', function(...) mockObj) - mockery::stub(aFIPC::autoFIPC, 'mirt::extract.mirt', function(...) list()) - mockery::stub(aFIPC::autoFIPC, 'mirt::multipleGroup', function(...) list()) - mockery::stub(aFIPC::autoFIPC, 'aFIPC::make_aFIPC_model', function(...) list()) + mockery::stub(aFIPC::autoFIPC, 'mirt::mirt', fake_model) + mockery::stub(aFIPC::autoFIPC, 'mirt::multipleGroup', fake_model) + mockery::stub(aFIPC::autoFIPC, 'aFIPC:::make_aFIPC_model', fake_model) test_data <- data.frame( item1 = c(1,0,1,0,1), From dacf3d96c6a7ce613219d402dd62511fc51eef2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:04:09 +0900 Subject: [PATCH 09/14] chore: restore package build-ignore authority --- .Rbuildignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index e3d14014..8989c62f 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -24,6 +24,3 @@ ^\.jules(/.*)?$ ^\.trivyignore\.yaml$ ^trivy\.yaml$ -^\.semgrepignore$ -^test_validation\.R$ -^\.markdownlint\.json$ From 296fd6e87eb673252160ae3a17343f623068adc4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:04:26 +0900 Subject: [PATCH 10/14] chore: restore sentinel guidance authority --- .jules/sentinel.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7d04c5ac..a8207a48 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,7 +2,3 @@ **Vulnerability:** Unvalidated inputs passed to `if()` statements can cause process crashes (`condition has length > 1`) or unexpected coercion vulnerabilities. **Learning:** In R, optional boolean parameters that default to `NULL` should be validated using explicit runtime type validation (e.g., `if (!is.null(flag) && (!is.logical(flag) || length(flag) != 1 || is.na(flag)))`). **Prevention:** Always implement explicit runtime type validation for optional boolean parameters. -## 2024-05-24 - [λŒ€ν™”ν˜• μž…λ ₯ 검증 μ‹œ μ •μˆ˜ μ˜€λ²„ν”Œλ‘œμš° λ°©μ§€] -**Vulnerability:** `readline()` ν•¨μˆ˜λ‘œ μ‚¬μš©μžμ˜ 숫자λ₯Ό μž…λ ₯받을 λ•Œ, `^[0-9]+$` μ •κ·œμ‹μ„ μ‚¬μš©ν•˜μ—¬ μž…λ ₯ 길이λ₯Ό μ œν•œν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 이둜 인해 ν—ˆμš© λ²”μœ„λ₯Ό μ΄ˆκ³Όν•˜λŠ” 맀우 큰 μˆ«μžκ°€ μž…λ ₯될 경우 `as.integer()`μ—μ„œ `NA`둜 ν‰κ°€λ˜μ–΄ 이후 ν”„λ‘œμ„ΈμŠ€μ—μ„œ ν¬λž˜μ‹œκ°€ λ°œμƒν•  수 μžˆλŠ” 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€. -**Learning:** μ‚¬μš©μž μž…λ ₯ 검증 μ‹œμ—λŠ” λ‹¨μˆœνžˆ μˆ«μžμΈμ§€ μ—¬λΆ€λ§Œ ν™•μΈν•˜λŠ” 것이 μ•„λ‹ˆλΌ, μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’(예: 1 λ˜λŠ” 2)λ§Œμ„ ν—ˆμš©ν•˜λ„λ‘ μ œν•œν•΄μ•Ό μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ•ˆμ •μ„±κ³Ό λ³΄μ•ˆμ„ μœ μ§€ν•  수 μžˆλ‹€λŠ” 것을 λ°°μ› μŠ΅λ‹ˆλ‹€. -**Prevention:** λŒ€ν™”ν˜• R μŠ€ν¬λ¦½νŠΈμ—μ„œ μž…λ ₯을 검증할 λ•ŒλŠ” `^[0-9]+$`와 같은 μ œν•œ μ—†λŠ” 숫자 클래슀 λŒ€μ‹  `^[12]$`와 같이 μ˜ˆμƒλ˜λŠ” μ •ν™•ν•œ κ°’κ³Ό μΌμΉ˜ν•˜λ„λ‘ μ •κ·œμ‹μ„ μ—„κ²©ν•˜κ²Œ μ •μ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. From c7a35c04767bcb85a590680bc10785942f53e0ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:04:31 +0900 Subject: [PATCH 11/14] chore: drop unrelated markdownlint policy --- .markdownlint.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index 6d68306c..00000000 --- a/.markdownlint.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "MD013": false, - "MD022": false, - "MD041": false -} From eb0ce2073053f7655d4aaa3b6d0ff5a61f059019 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:04:41 +0900 Subject: [PATCH 12/14] chore: restore package smoke source file --- test_dummy.R | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test_dummy.R diff --git a/test_dummy.R b/test_dummy.R new file mode 100644 index 00000000..e6f7019b --- /dev/null +++ b/test_dummy.R @@ -0,0 +1,2 @@ +source("R/aFIPC.R") +source("R/surveyFA.R") From 6403c54d3ad307e87e24618ad8b758f3c5165c74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:05:00 +0900 Subject: [PATCH 13/14] test: make interactive choice regression deterministic --- .../test-interactive-choice-validation.R | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/testthat/test-interactive-choice-validation.R diff --git a/tests/testthat/test-interactive-choice-validation.R b/tests/testthat/test-interactive-choice-validation.R new file mode 100644 index 00000000..6ddb4670 --- /dev/null +++ b/tests/testthat/test-interactive-choice-validation.R @@ -0,0 +1,77 @@ +test_that("common-item confirmation rejects values outside 1 or 2", { + mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) + mock_readline <- mockery::mock("0", "3", "12", cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, "readline", mock_readline) + + expect_error( + aFIPC::autoFIPC( + newformXData = data.frame(new_item_1 = c(1, 0, 1)), + oldformYData = data.frame(old_item_1 = c(0, 1, 0)), + newformCommonItemNames = "new_item_1", + oldformCommonItemNames = "old_item_1", + confirmCommonItems = NULL + ), + "Too many invalid common item confirmation attempts" + ) +}) + +test_that("old-form prior prompt rejects malformed and out-of-range choices", { + mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) + oversized_integer <- paste(rep("9", 1000), collapse = "") + mock_readline <- mockery::mock(" 1", "abc", oversized_integer, cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, "readline", mock_readline) + + test_data <- data.frame( + item1 = rep(c(0, 1), 50), + item2 = rep(c(1, 0), 50), + item3 = rep(c(0, 0, 1, 1), 25) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = c("item1", "item2"), + oldformCommonItemNames = c("item1", "item2"), + confirmCommonItems = TRUE, + itemtype = "3PL", + oldformBILOGprior = NULL + ), + "Too many invalid oldform BILOG prior attempts" + ) +}) + +test_that("new-form prior prompt rejects malformed and out-of-range choices", { + mockery::stub(aFIPC::autoFIPC, "interactive", TRUE) + oversized_integer <- paste(rep("9", 1000), collapse = "") + mock_readline <- mockery::mock("1", "3", "abc", oversized_integer, cycle = TRUE) + mockery::stub(aFIPC::autoFIPC, "readline", mock_readline) + + # autoFIPC reads these mirt slots directly before reaching the new-form prompt. + fake_model <- new("SingleGroupClass") + fake_model@OptimInfo <- list(secondordertest = TRUE) + fake_model@Data <- list(K = c(2, 2)) + + mockery::stub(aFIPC::autoFIPC, "mirt::mirt", fake_model) + mockery::stub(aFIPC::autoFIPC, "mirt::multipleGroup", fake_model) + mockery::stub(aFIPC::autoFIPC, "aFIPC:::make_aFIPC_model", fake_model) + + test_data <- data.frame( + item1 = c(1, 0, 1, 0, 1), + item2 = c(0, 1, 0, 1, 0) + ) + + expect_error( + aFIPC::autoFIPC( + newformXData = test_data, + oldformYData = test_data, + newformCommonItemNames = "item1", + oldformCommonItemNames = "item1", + confirmCommonItems = TRUE, + itemtype = "3PL", + oldformBILOGprior = NULL, + newformBILOGprior = NULL + ), + "Too many invalid newform BILOG prior attempts" + ) +}) From f68823c988433d0026b477211982bd3f9e2ab560 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:05:04 +0900 Subject: [PATCH 14/14] test: retire overstated overflow fixture --- .../testthat/test-sentinel-integer-overflow.R | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 tests/testthat/test-sentinel-integer-overflow.R diff --git a/tests/testthat/test-sentinel-integer-overflow.R b/tests/testthat/test-sentinel-integer-overflow.R deleted file mode 100644 index 490b06be..00000000 --- a/tests/testthat/test-sentinel-integer-overflow.R +++ /dev/null @@ -1,76 +0,0 @@ -test_that("readline validates 1 or 2 with exact regex strictly", { - mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) - mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) - - expect_error( - aFIPC::autoFIPC( - newformXData = data.frame(new_item_1=c(1,0,1)), - oldformYData = data.frame(old_item_1=c(0,1,0)), - newformCommonItemNames = c('new_item_1'), - oldformCommonItemNames = c('old_item_1'), - confirmCommonItems = NULL - ), - "Too many invalid common item confirmation attempts" - ) -}) - -test_that("readline validates 1 or 2 with exact regex strictly - oldformBILOGprior", { - mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) - mock_readline_fail <- mockery::mock("3", "999", "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) - - test_data <- data.frame( - item1 = sample(c(0, 1), 100, replace = TRUE), - item2 = sample(c(0, 1), 100, replace = TRUE), - item3 = sample(c(0, 1), 100, replace = TRUE) - ) - - expect_error( - aFIPC::autoFIPC( - newformXData = test_data, - oldformYData = test_data, - newformCommonItemNames = c('item1', 'item2'), - oldformCommonItemNames = c('item1', 'item2'), - confirmCommonItems = TRUE, - itemtype = '3PL', - oldformBILOGprior = NULL - ), - "Too many invalid oldform BILOG prior attempts" - ) -}) - -test_that("readline validates 1 or 2 with exact regex strictly - newformBILOGprior", { - mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE) - mock_readline_fail <- mockery::mock("1", "3", "999", "abc", cycle = TRUE) - mockery::stub(aFIPC::autoFIPC, 'readline', mock_readline_fail) - - # We actually need to build an S4 object with the `@` slots that mirt returns - # because autoFIPC accesses `@OptimInfo` directly instead of via extract.mirt! - fake_model <- new("SingleGroupClass") - fake_model@OptimInfo <- list(secondordertest = TRUE) - fake_model@Data <- list(K = c(2,2)) - - mockery::stub(aFIPC::autoFIPC, 'mirt::mirt', fake_model) - mockery::stub(aFIPC::autoFIPC, 'mirt::multipleGroup', fake_model) - mockery::stub(aFIPC::autoFIPC, 'aFIPC:::make_aFIPC_model', fake_model) - - test_data <- data.frame( - item1 = c(1,0,1,0,1), - item2 = c(0,1,0,1,0) - ) - - expect_error( - aFIPC::autoFIPC( - newformXData = test_data, - oldformYData = test_data, - newformCommonItemNames = c('item1'), - oldformCommonItemNames = c('item1'), - confirmCommonItems = TRUE, - itemtype = '3PL', - oldformBILOGprior = NULL, - newformBILOGprior = NULL - ), - "Too many invalid newform BILOG prior attempts" - ) -})